@miliastry/quasar 1.0.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/LICENSE +119 -0
- package/README.md +45 -0
- package/dist/Visuals/lyne.css +1027 -0
- package/dist/Visuals/osu.css +257 -0
- package/dist/index.d.mts +4563 -0
- package/dist/index.d.ts +4563 -0
- package/dist/index.js +11291 -0
- package/dist/index.mjs +11197 -0
- package/package.json +51 -0
- package/src/Analysis/Contracts/AnalysisReport.ts +26 -0
- package/src/Analysis/Contracts/Contribution.ts +76 -0
- package/src/Analysis/Contracts/Pass.ts +76 -0
- package/src/Analysis/Contracts/PipelineContext.ts +48 -0
- package/src/Analysis/Passes/Analysis/GradientAnalyzer.ts +292 -0
- package/src/Analysis/Passes/Analysis/MergeableColorAnalyzer.ts +112 -0
- package/src/Analysis/Passes/Analysis/RainbowAnalyzer.ts +233 -0
- package/src/Analysis/Passes/Analysis/WaveAnalyzer.ts +211 -0
- package/src/Analysis/Passes/Analysis/__tests__/GradientAnalyzer.test.ts +135 -0
- package/src/Analysis/Passes/Analysis/__tests__/MergeableColorAnalyzer.test.ts +84 -0
- package/src/Analysis/Passes/Analysis/__tests__/RainbowAnalyzer.test.ts +99 -0
- package/src/Analysis/Passes/Analysis/__tests__/WaveAnalyzer.test.ts +119 -0
- package/src/Analysis/Passes/Decision/DefaultDecision.ts +139 -0
- package/src/Analysis/Passes/Decision/__tests__/DefaultDecision.test.ts +179 -0
- package/src/Analysis/Passes/Transform/CollapseGradientTransform.ts +176 -0
- package/src/Analysis/Passes/Transform/MergeColorsTransform.ts +126 -0
- package/src/Analysis/Passes/Transform/RainbowCollapseTransform.ts +83 -0
- package/src/Analysis/Passes/Transform/WaveCollapseTransform.ts +88 -0
- package/src/Analysis/Passes/Utility/CharacterCountAnalyzer.ts +45 -0
- package/src/Analysis/Pipeline/Pipeline.ts +133 -0
- package/src/Analysis/Pipeline/PipelineBuilder.ts +55 -0
- package/src/Analysis/Pipeline/PipelineStage.ts +19 -0
- package/src/Analysis/Utils/color-utils.ts +132 -0
- package/src/Analysis/__tests__/Integration.test.ts +162 -0
- package/src/Analysis/__tests__/Pipeline.test.ts +133 -0
- package/src/Analysis/index.ts +52 -0
- package/src/BBCode/BBCodeDocumentModel.ts +175 -0
- package/src/BBCode/BBCodeToGreenNode.ts +755 -0
- package/src/BBCode/Parser.ts +384 -0
- package/src/BBCode/index.ts +12 -0
- package/src/Collab/positions.ts +91 -0
- package/src/Commands/Command.ts +44 -0
- package/src/Commands/CommandRegistry.ts +78 -0
- package/src/Commands/DeleteNode.ts +20 -0
- package/src/Commands/InsertText.ts +21 -0
- package/src/Commands/SplitMerge.ts +28 -0
- package/src/Commands/WrapInTag.ts +21 -0
- package/src/Commands/index.ts +6 -0
- package/src/Diff/TreeDiffer.ts +264 -0
- package/src/Diff/__tests__/TreeDiffer.test.ts +65 -0
- package/src/Diff/index.ts +2 -0
- package/src/Events/EventBus.ts +160 -0
- package/src/Events/index.ts +2 -0
- package/src/Formatter/Formatter.ts +54 -0
- package/src/Formatter/index.ts +2 -0
- package/src/HTML/HTMLDocumentModel.ts +35 -0
- package/src/HTML/HTMLToGreenNode.ts +290 -0
- package/src/Incremental/ChangeTracker.ts +105 -0
- package/src/Incremental/IncrementalParser.ts +591 -0
- package/src/Incremental/__tests__/IncrementalParser.test.ts +164 -0
- package/src/Incremental/index.ts +4 -0
- package/src/Lexer/BBCodeLexer.ts +382 -0
- package/src/Lexer/Lexer.ts +181 -0
- package/src/Lexer/index.ts +10 -0
- package/src/Linter/Linter.ts +193 -0
- package/src/Linter/index.ts +2 -0
- package/src/Markdown/MarkdownAST.ts +112 -0
- package/src/Markdown/MarkdownDocumentModel.ts +55 -0
- package/src/Markdown/MarkdownLexer.ts +203 -0
- package/src/Markdown/MarkdownParser.ts +455 -0
- package/src/Markdown/MarkdownToGreenNode.ts +153 -0
- package/src/Model/DocumentModel.ts +694 -0
- package/src/Model/NodeFactory.ts +117 -0
- package/src/Model/TagRegistry.ts +495 -0
- package/src/Model/index.ts +5 -0
- package/src/Plugins/PluginAPI.ts +119 -0
- package/src/Plugins/PluginRegistry.ts +132 -0
- package/src/Plugins/index.ts +3 -0
- package/src/Queries/QueryEngine.ts +152 -0
- package/src/Queries/index.ts +1 -0
- package/src/RenderPipeline/RenderPipeline.ts +125 -0
- package/src/RenderPipeline/RenderTree.ts +134 -0
- package/src/RenderPipeline/index.ts +4 -0
- package/src/Semantic/SemanticAnalyzer.ts +506 -0
- package/src/Semantic/index.ts +2 -0
- package/src/Symbols/SymbolTable.ts +124 -0
- package/src/Symbols/index.ts +1 -0
- package/src/Syntax/GreenNode.ts +324 -0
- package/src/Syntax/GreenNodePool.ts +269 -0
- package/src/Syntax/NodeMatcher.ts +370 -0
- package/src/Syntax/RedNode.ts +569 -0
- package/src/Syntax/RedNodeStore.ts +184 -0
- package/src/Syntax/TreeBuilder.ts +214 -0
- package/src/Syntax/__tests__/GreenNode.test.ts +33 -0
- package/src/Syntax/__tests__/RedNode.test.ts +81 -0
- package/src/Syntax/__tests__/RedNodeStore.test.ts +104 -0
- package/src/Syntax/greenEdit.ts +110 -0
- package/src/Syntax/hash.ts +30 -0
- package/src/Syntax/index.ts +12 -0
- package/src/Syntax/partition.ts +161 -0
- package/src/Syntax/preserveNodeIds.ts +201 -0
- package/src/Tests/ASTOptimizerIdempotence.test.ts +77 -0
- package/src/Tests/BlockPatcher.test.ts +437 -0
- package/src/Tests/BlockPatcherWindowed.test.ts +364 -0
- package/src/Tests/BoxDrawer.test.ts +217 -0
- package/src/Tests/BoxRichTitle.test.ts +105 -0
- package/src/Tests/Chars500kBenchmark.test.ts +151 -0
- package/src/Tests/Chars500kEdits.test.ts +321 -0
- package/src/Tests/CollabPositions.test.ts +146 -0
- package/src/Tests/CompilerPathProfiling.test.ts +186 -0
- package/src/Tests/DOMMorpher.test.ts +142 -0
- package/src/Tests/DomPatchPerf.test.ts +60 -0
- package/src/Tests/EffectSegments.snapshot.json +616 -0
- package/src/Tests/EffectSegments.test.ts +68 -0
- package/src/Tests/FindNodeAtOffset.test.ts +65 -0
- package/src/Tests/Fuzzer.test.ts +166 -0
- package/src/Tests/GreenNodePool.test.ts +153 -0
- package/src/Tests/Lexer.test.ts +238 -0
- package/src/Tests/LyneMode.test.ts +187 -0
- package/src/Tests/ModelCoherence.test.ts +180 -0
- package/src/Tests/Partition.test.ts +238 -0
- package/src/Tests/PluginTags.test.ts +150 -0
- package/src/Tests/ProblematicSection.test.ts +46 -0
- package/src/Tests/ProblematicSectionHTML.test.ts +58 -0
- package/src/Tests/RedReuse.test.ts +134 -0
- package/src/Tests/ReproDelete20k.test.ts +62 -0
- package/src/Tests/SemanticValidators.test.ts +136 -0
- package/src/Tests/StableNodeIds.test.ts +210 -0
- package/src/Tests/StudioColorBloat.test.ts +25 -0
- package/src/Tests/StudioDebugText.test.ts +27 -0
- package/src/Tests/StudioTrailingChar.test.ts +25 -0
- package/src/Tests/StudioValidText.test.ts +25 -0
- package/src/Tests/UrlImgBug.test.ts +23 -0
- package/src/Tests/VisualBuilderFidelity.test.ts +105 -0
- package/src/Tests/referenceDocument.ts +119 -0
- package/src/Transactions/Transaction.ts +176 -0
- package/src/Transactions/UndoManager.ts +111 -0
- package/src/Transactions/index.ts +3 -0
- package/src/Transformers/ASTOptimizer.ts +315 -0
- package/src/Transformers/GradientTransformer.ts +143 -0
- package/src/Transformers/GrowTransformer.ts +115 -0
- package/src/Transformers/RainbowTransformer.ts +121 -0
- package/src/Transformers/SineWaveTransformer.ts +130 -0
- package/src/Transformers/Transformer.ts +22 -0
- package/src/Types/core.ts +270 -0
- package/src/Types/diagnostics.ts +156 -0
- package/src/Types/index.ts +23 -0
- package/src/Types/operations.ts +180 -0
- package/src/Types/queries.ts +121 -0
- package/src/Types/symbols.ts +69 -0
- package/src/Types/tokens.ts +186 -0
- package/src/Utils/BBCodeGenerator.ts +126 -0
- package/src/Utils/ColorMath.ts +276 -0
- package/src/Utils/color.ts +112 -0
- package/src/Utils/dom-to-svg.test.ts +86 -0
- package/src/Utils/dom-to-svg.ts +615 -0
- package/src/Utils/treeTransformers.ts +717 -0
- package/src/Visitors/BBBlocksExporter.ts +69 -0
- package/src/Visitors/BBCodeExporter.ts +318 -0
- package/src/Visitors/BlockPatcher.ts +963 -0
- package/src/Visitors/DOMMorpher.ts +134 -0
- package/src/Visitors/HTMLRenderer.ts +1077 -0
- package/src/Visitors/JSONExporter.ts +66 -0
- package/src/Visitors/MarkdownExporter.ts +99 -0
- package/src/Visitors/SVGRenderer.ts +35 -0
- package/src/Visitors/TiptapExporter.ts +145 -0
- package/src/Visitors/Visitor.ts +48 -0
- package/src/Visitors/index.ts +9 -0
- package/src/Visuals/BoxDrawer.ts +175 -0
- package/src/Visuals/index.ts +42 -0
- package/src/Visuals/lyne.css +1027 -0
- package/src/Visuals/osu.css +257 -0
- package/src/index.ts +197 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Quasar Studio — Color Math & Utilities
|
|
3
|
+
// Ported from TextStudio to provide native algorithms for Quasar
|
|
4
|
+
// ============================================================
|
|
5
|
+
|
|
6
|
+
export type Easing = "linear" | "easeIn" | "easeOut" | "easeInOut" | "easeIO" | "parabola" | "half-parabola" | string // string allows 'bezier(0.25,0.1,0.25,1.0)'
|
|
7
|
+
|
|
8
|
+
export function solveCubicBezierY(x: number, x1: number, y1: number, x2: number, y2: number): number {
|
|
9
|
+
if (x <= 0) return 0;
|
|
10
|
+
if (x >= 1) return 1;
|
|
11
|
+
let lower = 0;
|
|
12
|
+
let upper = 1;
|
|
13
|
+
let t = 0.5;
|
|
14
|
+
for (let i = 0; i < 15; i++) {
|
|
15
|
+
const currentX = 3 * Math.pow(1 - t, 2) * t * x1 + 3 * (1 - t) * Math.pow(t, 2) * x2 + Math.pow(t, 3);
|
|
16
|
+
if (Math.abs(currentX - x) < 0.001) break;
|
|
17
|
+
if (currentX < x) lower = t;
|
|
18
|
+
else upper = t;
|
|
19
|
+
t = (lower + upper) / 2;
|
|
20
|
+
}
|
|
21
|
+
return 3 * Math.pow(1 - t, 2) * t * y1 + 3 * (1 - t) * Math.pow(t, 2) * y2 + Math.pow(t, 3);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Bezier easing approximation or simple evaluation
|
|
25
|
+
export function ease(t: number, easing: Easing, center: number = 0.5, power: number = 2): number {
|
|
26
|
+
t = Math.max(0, Math.min(1, t))
|
|
27
|
+
|
|
28
|
+
if (easing.startsWith('expr(')) {
|
|
29
|
+
// to be fixed, dont touch or use it
|
|
30
|
+
const expr = easing.slice(5, -1);
|
|
31
|
+
try {
|
|
32
|
+
const evaluate = new Function('x', `with(Math) { return ${expr}; }`);
|
|
33
|
+
return Number(evaluate(t)) || 0;
|
|
34
|
+
} catch (e) {
|
|
35
|
+
return t;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (easing.startsWith('bezier')) {
|
|
40
|
+
const match = easing.match(/bezier\(([^,]+),([^,]+),([^,]+),([^)]+)\)/);
|
|
41
|
+
if (match) {
|
|
42
|
+
const x1 = parseFloat(match[1]);
|
|
43
|
+
const y1 = parseFloat(match[2]);
|
|
44
|
+
const x2 = parseFloat(match[3]);
|
|
45
|
+
const y2 = parseFloat(match[4]);
|
|
46
|
+
return solveCubicBezierY(t, x1, y1, x2, y2);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
switch (easing) {
|
|
50
|
+
case "easeIn":
|
|
51
|
+
case "half-parabola": return Math.pow(t, power)
|
|
52
|
+
case "easeOut": return t * (2 - t)
|
|
53
|
+
case "easeInOut":
|
|
54
|
+
case "easeIO":
|
|
55
|
+
return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t
|
|
56
|
+
case "parabola":
|
|
57
|
+
if (center <= 0) return Math.pow(t, power);
|
|
58
|
+
if (center >= 1) return Math.pow(1 - t, power);
|
|
59
|
+
return t < center
|
|
60
|
+
? Math.pow((center - t) / center, power)
|
|
61
|
+
: Math.pow((t - center) / (1 - center), power);
|
|
62
|
+
default: return t
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
export function hslToHex(h: number, s: number, l: number): string {
|
|
68
|
+
l /= 100
|
|
69
|
+
const a = (s * Math.min(l, 1 - l)) / 100
|
|
70
|
+
const f = (n: number) => {
|
|
71
|
+
const k = (n + h / 30) % 12
|
|
72
|
+
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1)
|
|
73
|
+
return Math.round(255 * color).toString(16).padStart(2, "0")
|
|
74
|
+
}
|
|
75
|
+
return `#${f(0)}${f(8)}${f(4)}`
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function hexToRgb(hex: string): [number, number, number] {
|
|
79
|
+
const h = hex.replace("#", "")
|
|
80
|
+
if (h.length === 3) {
|
|
81
|
+
return [
|
|
82
|
+
parseInt(h[0] + h[0], 16),
|
|
83
|
+
parseInt(h[1] + h[1], 16),
|
|
84
|
+
parseInt(h[2] + h[2], 16),
|
|
85
|
+
]
|
|
86
|
+
}
|
|
87
|
+
return [
|
|
88
|
+
parseInt(h.slice(0, 2), 16),
|
|
89
|
+
parseInt(h.slice(2, 4), 16),
|
|
90
|
+
parseInt(h.slice(4, 6), 16),
|
|
91
|
+
]
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function hexToHsl(hex: string): [number, number, number] {
|
|
95
|
+
let [r, g, b] = hexToRgb(hex);
|
|
96
|
+
r /= 255;
|
|
97
|
+
g /= 255;
|
|
98
|
+
b /= 255;
|
|
99
|
+
const max = Math.max(r, g, b), min = Math.min(r, g, b);
|
|
100
|
+
let h = 0, s = 0, l = (max + min) / 2;
|
|
101
|
+
|
|
102
|
+
if (max !== min) {
|
|
103
|
+
const d = max - min;
|
|
104
|
+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
105
|
+
switch (max) {
|
|
106
|
+
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
|
|
107
|
+
case g: h = (b - r) / d + 2; break;
|
|
108
|
+
case b: h = (r - g) / d + 4; break;
|
|
109
|
+
}
|
|
110
|
+
h /= 6;
|
|
111
|
+
}
|
|
112
|
+
return [h * 360, s * 100, l * 100];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function mixHex(color1: string, color2: string, weight: number): string {
|
|
116
|
+
const [r1, g1, b1] = hexToRgb(color1)
|
|
117
|
+
const [r2, g2, b2] = hexToRgb(color2)
|
|
118
|
+
const w = Math.max(0, Math.min(1, weight))
|
|
119
|
+
const r = Math.round(r1 + (r2 - r1) * w)
|
|
120
|
+
const g = Math.round(g1 + (g2 - g1) * w)
|
|
121
|
+
const b = Math.round(b1 + (b2 - b1) * w)
|
|
122
|
+
return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function mixMultiple(colors: string[], weight: number): string {
|
|
126
|
+
if (colors.length === 0) return "#FFFFFF"
|
|
127
|
+
if (colors.length === 1) return colors[0]
|
|
128
|
+
weight = Math.max(0, Math.min(1, weight || 0))
|
|
129
|
+
const scaled = weight * (colors.length - 1)
|
|
130
|
+
const index = Math.floor(scaled)
|
|
131
|
+
if (index >= colors.length - 1) return colors[colors.length - 1]
|
|
132
|
+
const frac = scaled - index
|
|
133
|
+
return mixHex(colors[index], colors[index + 1], frac)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface ColorStop {
|
|
137
|
+
color: string;
|
|
138
|
+
position: number; // 0 to 1
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function mixMultipleStops(stops: ColorStop[], weight: number): string {
|
|
142
|
+
if (stops.length === 0) return "#FFFFFF";
|
|
143
|
+
if (stops.length === 1) return stops[0].color;
|
|
144
|
+
weight = Math.max(0, Math.min(1, weight || 0));
|
|
145
|
+
|
|
146
|
+
let i = 0;
|
|
147
|
+
while (i < stops.length - 1 && stops[i + 1].position <= weight) {
|
|
148
|
+
i++;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (i >= stops.length - 1) return stops[stops.length - 1].color;
|
|
152
|
+
if (weight <= stops[i].position) return stops[i].color;
|
|
153
|
+
|
|
154
|
+
const range = stops[i + 1].position - stops[i].position;
|
|
155
|
+
const frac = (weight - stops[i].position) / range;
|
|
156
|
+
return mixHex(stops[i].color, stops[i + 1].color, frac);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ═══════════════════════════════════════════════════════════════
|
|
160
|
+
// OKLab — Perceptual Color Space
|
|
161
|
+
// ═══════════════════════════════════════════════════════════════
|
|
162
|
+
//
|
|
163
|
+
// OKLab is a color space designed by Björn Ottosson in 2020 to be
|
|
164
|
+
// perceptually uniform: equal Euclidean distance ≈ equal perceived
|
|
165
|
+
// color difference. This makes it ideal for gradient detection.
|
|
166
|
+
//
|
|
167
|
+
// Reference: https://bottosson.github.io/posts/oklab/
|
|
168
|
+
//
|
|
169
|
+
// ═══════════════════════════════════════════════════════════════
|
|
170
|
+
|
|
171
|
+
// Linear sRGB → LMS (cone response) matrix
|
|
172
|
+
const SRGB_TO_LMS = [
|
|
173
|
+
0.4122214708, 0.5363325363, 0.0514459929,
|
|
174
|
+
0.2119034982, 0.6806995451, 0.1073969566,
|
|
175
|
+
0.0883024619, 0.2817188376, 0.6299787005,
|
|
176
|
+
] as const
|
|
177
|
+
|
|
178
|
+
// LMS → OKLab matrix
|
|
179
|
+
const LMS_TO_OKLAB = [
|
|
180
|
+
0.2104542553, 0.7936177850, -0.0040720468,
|
|
181
|
+
1.9779984951, -2.4285922050, 0.4505937099,
|
|
182
|
+
0.0259040371, 0.7827717662, -0.8086757660,
|
|
183
|
+
] as const
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Convert sRGB [0-1] to linear RGB (gamma expansion).
|
|
187
|
+
*/
|
|
188
|
+
function srgbToLinear(c: number): number {
|
|
189
|
+
return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Convert linear RGB to sRGB (gamma compression for display).
|
|
194
|
+
*/
|
|
195
|
+
function linearToSrgb(c: number): number {
|
|
196
|
+
return c <= 0.0031308 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Convert a hex colour to OKLab [L, a, b] components.
|
|
201
|
+
* L (lightness) in [0, 1], a/b (opponent axes) roughly in [-0.4, 0.4].
|
|
202
|
+
*/
|
|
203
|
+
export function hexToOklab(hex: string): [number, number, number] {
|
|
204
|
+
const [r255, g255, b255] = hexToRgb(hex)
|
|
205
|
+
const r = srgbToLinear(r255 / 255)
|
|
206
|
+
const g = srgbToLinear(g255 / 255)
|
|
207
|
+
const b = srgbToLinear(b255 / 255)
|
|
208
|
+
|
|
209
|
+
// Linear sRGB → LMS
|
|
210
|
+
const l_ = r * SRGB_TO_LMS[0] + g * SRGB_TO_LMS[1] + b * SRGB_TO_LMS[2]
|
|
211
|
+
const m_ = r * SRGB_TO_LMS[3] + g * SRGB_TO_LMS[4] + b * SRGB_TO_LMS[5]
|
|
212
|
+
const s_ = r * SRGB_TO_LMS[6] + g * SRGB_TO_LMS[7] + b * SRGB_TO_LMS[8]
|
|
213
|
+
|
|
214
|
+
// Non-linear transform (cube root)
|
|
215
|
+
const l = Math.cbrt(l_)
|
|
216
|
+
const m = Math.cbrt(m_)
|
|
217
|
+
const s = Math.cbrt(s_)
|
|
218
|
+
|
|
219
|
+
// LMS → OKLab
|
|
220
|
+
return [
|
|
221
|
+
l * LMS_TO_OKLAB[0] + m * LMS_TO_OKLAB[1] + s * LMS_TO_OKLAB[2],
|
|
222
|
+
l * LMS_TO_OKLAB[3] + m * LMS_TO_OKLAB[4] + s * LMS_TO_OKLAB[5],
|
|
223
|
+
l * LMS_TO_OKLAB[6] + m * LMS_TO_OKLAB[7] + s * LMS_TO_OKLAB[8],
|
|
224
|
+
]
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Calculate the perceptual distance between two hex colours using OKLab.
|
|
229
|
+
* Returns a value in [0, ~0.4] where:
|
|
230
|
+
* < 0.02 ≈ imperceptible difference
|
|
231
|
+
* < 0.05 ≈ small but noticeable
|
|
232
|
+
* > 0.1 ≈ very noticeable
|
|
233
|
+
*/
|
|
234
|
+
export function perceptualDistance(hex1: string, hex2: string): number {
|
|
235
|
+
const [L1, a1, b1] = hexToOklab(hex1)
|
|
236
|
+
const [L2, a2, b2] = hexToOklab(hex2)
|
|
237
|
+
const dL = L2 - L1
|
|
238
|
+
const da = a2 - a1
|
|
239
|
+
const db = b2 - b1
|
|
240
|
+
return Math.sqrt(dL * dL + da * da + db * db)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Mix two hex colours in OKLab space for perceptually uniform interpolation.
|
|
245
|
+
*/
|
|
246
|
+
export function mixHexOklab(color1: string, color2: string, weight: number): string {
|
|
247
|
+
const [L1, a1, b1] = hexToOklab(color1)
|
|
248
|
+
const [L2, a2, b2] = hexToOklab(color2)
|
|
249
|
+
const w = Math.max(0, Math.min(1, weight))
|
|
250
|
+
|
|
251
|
+
const L = L1 + (L2 - L1) * w
|
|
252
|
+
const a = a1 + (a2 - a1) * w
|
|
253
|
+
const b = b1 + (b2 - b1) * w
|
|
254
|
+
|
|
255
|
+
// OKLab → LMS
|
|
256
|
+
const l_ = L + a * 0.3963377774 + b * 0.2158037573
|
|
257
|
+
const m_ = L - a * 0.1055613458 - b * 0.0638541728
|
|
258
|
+
const s_ = L - a * 0.0894841775 - b * 1.2914855480
|
|
259
|
+
|
|
260
|
+
// Cube (inverse of cube root)
|
|
261
|
+
const l = l_ * l_ * l_
|
|
262
|
+
const m = m_ * m_ * m_
|
|
263
|
+
const s = s_ * s_ * s_
|
|
264
|
+
|
|
265
|
+
// LMS → linear sRGB
|
|
266
|
+
const rLin = l * 4.0767416621 + m * -3.3077115913 + s * 0.2309699292
|
|
267
|
+
const gLin = l * -1.2684380046 + m * 2.6097574011 + s * -0.3413193965
|
|
268
|
+
const bLin = l * -0.0041960863 + m * -0.7034186147 + s * 1.7076147010
|
|
269
|
+
|
|
270
|
+
// Linear sRGB → gamma-corrected sRGB → hex
|
|
271
|
+
const r255 = Math.round(linearToSrgb(rLin) * 255)
|
|
272
|
+
const g255 = Math.round(linearToSrgb(gLin) * 255)
|
|
273
|
+
const b255 = Math.round(linearToSrgb(bLin) * 255)
|
|
274
|
+
|
|
275
|
+
return `#${r255.toString(16).padStart(2, '0')}${g255.toString(16).padStart(2, '0')}${b255.toString(16).padStart(2, '0')}`
|
|
276
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quasar — Color Utilities
|
|
3
|
+
*
|
|
4
|
+
* Standalone color functions for gradient/grow/exporter interpolation.
|
|
5
|
+
* No external dependencies — pure math.
|
|
6
|
+
*
|
|
7
|
+
* Ported from TextStudio/lib/color.ts (originally backed by culori).
|
|
8
|
+
* These are the minimal subset needed for internal tag export.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// ─── Types ──────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
export type Easing = 'linear' | 'easeIn' | 'easeOut' | 'easeInOut'
|
|
14
|
+
|
|
15
|
+
// ─── Parsing ────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
/** Parse "#RRGGBB" to [r, g, b] each in [0, 255] */
|
|
18
|
+
function parseHex(hex: string): [number, number, number] {
|
|
19
|
+
const h = hex.replace('#', '')
|
|
20
|
+
return [
|
|
21
|
+
parseInt(h.substring(0, 2), 16),
|
|
22
|
+
parseInt(h.substring(2, 4), 16),
|
|
23
|
+
parseInt(h.substring(4, 6), 16),
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Format [r, g, b] (0-255) to "#RRGGBB" */
|
|
28
|
+
function toHex(r: number, g: number, b: number): string {
|
|
29
|
+
const clamp = (v: number) => Math.max(0, Math.min(255, Math.round(v)))
|
|
30
|
+
return (
|
|
31
|
+
'#' +
|
|
32
|
+
clamp(r).toString(16).padStart(2, '0') +
|
|
33
|
+
clamp(g).toString(16).padStart(2, '0') +
|
|
34
|
+
clamp(b).toString(16).padStart(2, '0')
|
|
35
|
+
).toUpperCase()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ─── Interpolation ──────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Mix two hex colors via linear RGB interpolation.
|
|
42
|
+
* t in [0, 1] — 0 = from, 1 = to.
|
|
43
|
+
*/
|
|
44
|
+
export function mixHex(from: string, to: string, t: number): string {
|
|
45
|
+
const [r1, g1, b1] = parseHex(from)
|
|
46
|
+
const [r2, g2, b2] = parseHex(to)
|
|
47
|
+
return toHex(
|
|
48
|
+
r1 + (r2 - r1) * t,
|
|
49
|
+
g1 + (g2 - g1) * t,
|
|
50
|
+
b1 + (b2 - b1) * t,
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Mix across multiple hex colors.
|
|
56
|
+
* t in [0, 1] maps across the color array linearly.
|
|
57
|
+
*/
|
|
58
|
+
export function mixMultiple(colors: string[], t: number): string {
|
|
59
|
+
if (colors.length === 0) return '#FFFFFF'
|
|
60
|
+
if (colors.length === 1) return colors[0]
|
|
61
|
+
const segments = colors.length - 1
|
|
62
|
+
const segment = Math.min(Math.floor(t * segments), segments - 1)
|
|
63
|
+
const localFactor = t * segments - segment
|
|
64
|
+
return mixHex(colors[segment], colors[segment + 1], localFactor)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ─── HSL → Hex ──────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Convert HSL to hex string.
|
|
71
|
+
* @param h hue in [0, 360]
|
|
72
|
+
* @param s saturation in [0, 1]
|
|
73
|
+
* @param l lightness in [0, 1]
|
|
74
|
+
*/
|
|
75
|
+
export function hslToHex(h: number, s: number, l: number): string {
|
|
76
|
+
const hue = ((h % 360) + 360) % 360
|
|
77
|
+
const sat = Math.max(0, Math.min(1, s))
|
|
78
|
+
const lit = Math.max(0, Math.min(1, l))
|
|
79
|
+
|
|
80
|
+
const c = (1 - Math.abs(2 * lit - 1)) * sat
|
|
81
|
+
const x = c * (1 - Math.abs(((hue / 60) % 2) - 1))
|
|
82
|
+
const m = lit - c / 2
|
|
83
|
+
|
|
84
|
+
let r = 0, g = 0, b = 0
|
|
85
|
+
if (hue < 60) { r = c; g = x; }
|
|
86
|
+
else if (hue < 120) { r = x; g = c; }
|
|
87
|
+
else if (hue < 180) { g = c; b = x; }
|
|
88
|
+
else if (hue < 240) { g = x; b = c; }
|
|
89
|
+
else if (hue < 300) { r = x; b = c; }
|
|
90
|
+
else { r = c; b = x; }
|
|
91
|
+
|
|
92
|
+
return toHex((r + m) * 255, (g + m) * 255, (b + m) * 255)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ─── Easing ─────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
export function clamp(n: number, min = 0, max = 1): number {
|
|
98
|
+
return Math.min(max, Math.max(min, n))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function ease(t: number, kind: Easing): number {
|
|
102
|
+
switch (kind) {
|
|
103
|
+
case 'easeIn':
|
|
104
|
+
return t * t
|
|
105
|
+
case 'easeOut':
|
|
106
|
+
return 1 - (1 - t) * (1 - t)
|
|
107
|
+
case 'easeInOut':
|
|
108
|
+
return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2
|
|
109
|
+
default:
|
|
110
|
+
return t
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import { domToSVGResult } from './dom-to-svg'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* El vectorizador del Studio Vector no debe exportar los overlays de EDICIÓN
|
|
6
|
+
* del imagemap (las regiones clicables ni su tooltip de hover): son UI del
|
|
7
|
+
* preview, no contenido real. Antes el skip buscaba la clase legacy
|
|
8
|
+
* `bb-imagemap-area`, pero el HTMLRenderer genera `imagemap-area
|
|
9
|
+
* bbcode-imap-area` — las clases no coincidían y las regiones se colaban en
|
|
10
|
+
* el SVG. El contenedor y la imagen SÍ deben salir.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const IMAGEMAP_HTML = `
|
|
14
|
+
<div class="imagemap-container bbcode-imagemap" style="position:relative;display:inline-block;width:200px;height:100px;">
|
|
15
|
+
<img src="https://example.com/map.png" alt="imagemap" style="width:200px;height:100px;display:block;">
|
|
16
|
+
<a class="imagemap-area bbcode-imap-area" href="https://example.com/1" style="position:absolute;left:10%;top:10%;width:30%;height:30%;"></a>
|
|
17
|
+
<a class="imagemap-area bbcode-imap-area" href="https://example.com/2" style="position:absolute;left:50%;top:10%;width:30%;height:30%;"></a>
|
|
18
|
+
<div class="bb-imagemap-tooltip" style="position:absolute;">tooltip</div>
|
|
19
|
+
</div>
|
|
20
|
+
`
|
|
21
|
+
|
|
22
|
+
/** jsdom no hace layout: rects fake según la posición/el tamaño declarados. */
|
|
23
|
+
function mockLayout(root: HTMLElement) {
|
|
24
|
+
const rectFor = (el: HTMLElement): DOMRect => {
|
|
25
|
+
const style = window.getComputedStyle(el)
|
|
26
|
+
const left = parseFloat(style.left) || 0
|
|
27
|
+
const top = parseFloat(style.top) || 0
|
|
28
|
+
const width = parseFloat(style.width) || (el.tagName === 'IMG' ? 200 : 100)
|
|
29
|
+
const height = parseFloat(style.height) || (el.tagName === 'IMG' ? 100 : 50)
|
|
30
|
+
return new DOMRect(left, top, width, height)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const originals = new Map<Element, () => DOMRect>()
|
|
34
|
+
const walker = (node: Element | null) => {
|
|
35
|
+
if (!node) return
|
|
36
|
+
originals.set(node, node.getBoundingClientRect.bind(node))
|
|
37
|
+
node.getBoundingClientRect = () => rectFor(node as HTMLElement)
|
|
38
|
+
Array.from(node.children).forEach(walker)
|
|
39
|
+
}
|
|
40
|
+
walker(root)
|
|
41
|
+
|
|
42
|
+
return () => originals.forEach((orig, el) => {
|
|
43
|
+
el.getBoundingClientRect = orig
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe('dom-to-svg · imagemap overlays', () => {
|
|
48
|
+
let restore: () => void
|
|
49
|
+
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
document.body.innerHTML = IMAGEMAP_HTML
|
|
52
|
+
restore = mockLayout(document.body.firstElementChild as HTMLElement)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
afterEach(() => {
|
|
56
|
+
restore()
|
|
57
|
+
document.body.innerHTML = ''
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('no exporta las regiones clicables (imagemap-area bbcode-imap-area)', () => {
|
|
61
|
+
const result = domToSVGResult(document.body.firstElementChild as HTMLElement, {
|
|
62
|
+
backgroundColor: '#1c1719',
|
|
63
|
+
scale: 1,
|
|
64
|
+
})
|
|
65
|
+
// Las áreas llevan href de las regiones: su ausencia es la prueba.
|
|
66
|
+
expect(result.svg).not.toContain('example.com/1')
|
|
67
|
+
expect(result.svg).not.toContain('example.com/2')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('no exporta el tooltip de hover (bb-imagemap-tooltip)', () => {
|
|
71
|
+
const result = domToSVGResult(document.body.firstElementChild as HTMLElement, {
|
|
72
|
+
backgroundColor: '#1c1719',
|
|
73
|
+
scale: 1,
|
|
74
|
+
})
|
|
75
|
+
expect(result.svg).not.toContain('tooltip')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('sí exporta la imagen real del imagemap', () => {
|
|
79
|
+
const result = domToSVGResult(document.body.firstElementChild as HTMLElement, {
|
|
80
|
+
backgroundColor: '#1c1719',
|
|
81
|
+
scale: 1,
|
|
82
|
+
})
|
|
83
|
+
expect(result.svg).toContain('map.png')
|
|
84
|
+
expect(result.svg).toContain('<image')
|
|
85
|
+
})
|
|
86
|
+
})
|