socket-function 0.9.0 → 0.9.1
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/.eslintrc.js +50 -50
- package/SocketFunction.ts +280 -280
- package/SocketFunctionTypes.ts +90 -90
- package/hot/HotReloadController.ts +105 -105
- package/mobx/UrlParam.ts +39 -39
- package/mobx/observer.tsx +49 -49
- package/mobx/promiseToObservable.tsx +41 -41
- package/package.json +30 -28
- package/require/CSSShim.ts +19 -19
- package/require/RequireController.ts +252 -252
- package/require/buffer.js +2368 -2368
- package/require/compileFlags.ts +44 -44
- package/require/require.html +13 -13
- package/require/require.js +462 -456
- package/spec.txt +115 -115
- package/src/CallFactory.ts +389 -389
- package/src/JSONLACKS/JSONLACKS.generated.js +17 -17
- package/src/JSONLACKS/JSONLACKS.pegjs +247 -247
- package/src/JSONLACKS/JSONLACKS.ts +429 -375
- package/src/args.ts +21 -21
- package/src/batching.ts +170 -129
- package/src/caching.ts +318 -314
- package/src/callHTTPHandler.ts +203 -203
- package/src/callManager.ts +134 -134
- package/src/certStore.ts +29 -29
- package/src/fixLargeNetworkCalls.ts +8 -8
- package/src/formatting/colors.ts +78 -78
- package/src/formatting/format.ts +160 -156
- package/src/formatting/logColors.ts +17 -17
- package/src/misc.ts +302 -171
- package/src/nodeCache.ts +92 -92
- package/src/nodeProxy.ts +54 -54
- package/src/profiling/getOwnTime.ts +142 -142
- package/src/profiling/measure.ts +273 -244
- package/src/profiling/stats.ts +212 -212
- package/src/profiling/tcpLagProxy.ts +63 -63
- package/src/storagePath.ts +10 -10
- package/src/tlsParsing.ts +96 -96
- package/src/types.ts +8 -8
- package/src/webSocketServer.ts +250 -250
- package/test/client.css +2 -2
- package/test/client.ts +46 -46
- package/test/server.ts +43 -43
- package/test/shared.ts +52 -52
- package/tsconfig.json +26 -26
package/src/callManager.ts
CHANGED
|
@@ -1,135 +1,135 @@
|
|
|
1
|
-
import { CallerContext, CallType, ClientHookContext, FullCallType, HookContext, SocketExposedInterface, SocketExposedInterfaceClass, SocketExposedShape, SocketFunctionClientHook, SocketFunctionHook, SocketRegistered } from "../SocketFunctionTypes";
|
|
2
|
-
import { _setSocketContext } from "../SocketFunction";
|
|
3
|
-
import { isNode } from "./misc";
|
|
4
|
-
import debugbreak from "debugbreak";
|
|
5
|
-
import { measureWrap } from "./profiling/measure";
|
|
6
|
-
|
|
7
|
-
let classes: {
|
|
8
|
-
[classGuid: string]: {
|
|
9
|
-
controller: SocketExposedInterface;
|
|
10
|
-
shape: SocketExposedShape;
|
|
11
|
-
}
|
|
12
|
-
} = {};
|
|
13
|
-
let exposedClasses = new Set<string>();
|
|
14
|
-
|
|
15
|
-
let globalHooks: SocketFunctionHook[] = [];
|
|
16
|
-
let globalClientHooks: SocketFunctionClientHook[] = [];
|
|
17
|
-
|
|
18
|
-
export async function performLocalCall(
|
|
19
|
-
config: {
|
|
20
|
-
call: FullCallType;
|
|
21
|
-
caller: CallerContext;
|
|
22
|
-
}
|
|
23
|
-
): Promise<unknown> {
|
|
24
|
-
const { call, caller } = config;
|
|
25
|
-
let classDef = classes[call.classGuid];
|
|
26
|
-
|
|
27
|
-
if (!classDef) {
|
|
28
|
-
throw new Error(`Class ${call.classGuid} not found`);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
if (!exposedClasses.has(call.classGuid)) {
|
|
32
|
-
throw new Error(`Class ${call.classGuid} not exposed`);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
let controller = classDef.controller;
|
|
36
|
-
let functionShape = classDef.shape[call.functionName];
|
|
37
|
-
if (!functionShape) {
|
|
38
|
-
throw new Error(`Function ${call.functionName} not exposed`);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
if (!controller[call.functionName]) {
|
|
42
|
-
throw new Error(`Function ${call.functionName} does not exist`);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
let serverContext = await runServerHooks(call, caller, functionShape);
|
|
46
|
-
if ("overrideResult" in serverContext) {
|
|
47
|
-
return serverContext.overrideResult;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// NOTE: We purposely don't await inside _setSocketContext, so the context is reset synchronously
|
|
51
|
-
let result = _setSocketContext(caller, () => {
|
|
52
|
-
return controller[call.functionName](...call.args);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
return await result;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function isDataImmutable(call: CallType) {
|
|
59
|
-
return !!classes[call.classGuid]?.shape[call.functionName]?.dataImmutable;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function registerClass(classGuid: string, controller: SocketExposedInterface, shape: SocketExposedShape) {
|
|
63
|
-
if (classes[classGuid]) {
|
|
64
|
-
throw new Error(`Class ${classGuid} already registered`);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
classes[classGuid] = {
|
|
68
|
-
controller,
|
|
69
|
-
shape,
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function exposeClass(exposedClass: SocketRegistered) {
|
|
74
|
-
exposedClasses.add(exposedClass._classGuid);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export function registerGlobalHook(hook: SocketFunctionHook) {
|
|
78
|
-
globalHooks.push(hook);
|
|
79
|
-
}
|
|
80
|
-
export function unregisterGlobalHook(hook: SocketFunctionHook) {
|
|
81
|
-
let index = globalHooks.indexOf(hook);
|
|
82
|
-
if (index >= 0) {
|
|
83
|
-
globalHooks.splice(index, 1);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
export function registerGlobalClientHook(hook: SocketFunctionClientHook) {
|
|
87
|
-
globalClientHooks.push(hook);
|
|
88
|
-
}
|
|
89
|
-
export function unregisterGlobalClientHook(hook: SocketFunctionClientHook) {
|
|
90
|
-
let index = globalClientHooks.indexOf(hook);
|
|
91
|
-
if (index >= 0) {
|
|
92
|
-
globalClientHooks.splice(index, 1);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export const runClientHooks = measureWrap(async function runClientHooks(
|
|
97
|
-
callType: FullCallType,
|
|
98
|
-
hooks: SocketExposedShape[""],
|
|
99
|
-
connectionId: { nodeId: string },
|
|
100
|
-
): Promise<ClientHookContext> {
|
|
101
|
-
let context: ClientHookContext = { call: callType, connectionId };
|
|
102
|
-
|
|
103
|
-
let clientHooks = (
|
|
104
|
-
globalClientHooks
|
|
105
|
-
.concat(hooks.clientHooks || [])
|
|
106
|
-
);
|
|
107
|
-
for (let otherClientHook of globalHooks.concat(hooks.hooks || []).map(x => x.clientHook)) {
|
|
108
|
-
if (otherClientHook) {
|
|
109
|
-
clientHooks.push(otherClientHook);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
for (let hook of clientHooks) {
|
|
113
|
-
await hook(context);
|
|
114
|
-
if ("overrideResult" in context) {
|
|
115
|
-
break;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
return context;
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
export const runServerHooks = measureWrap(async function runServerHooks(
|
|
123
|
-
callType: FullCallType,
|
|
124
|
-
caller: CallerContext,
|
|
125
|
-
hooks: SocketExposedShape[""],
|
|
126
|
-
): Promise<HookContext> {
|
|
127
|
-
let hookContext: HookContext = { call: callType };
|
|
128
|
-
for (let hook of globalHooks.concat(hooks.hooks || [])) {
|
|
129
|
-
await _setSocketContext(caller, () => hook(hookContext));
|
|
130
|
-
if ("overrideResult" in hookContext) {
|
|
131
|
-
break;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
return hookContext;
|
|
1
|
+
import { CallerContext, CallType, ClientHookContext, FullCallType, HookContext, SocketExposedInterface, SocketExposedInterfaceClass, SocketExposedShape, SocketFunctionClientHook, SocketFunctionHook, SocketRegistered } from "../SocketFunctionTypes";
|
|
2
|
+
import { _setSocketContext } from "../SocketFunction";
|
|
3
|
+
import { isNode } from "./misc";
|
|
4
|
+
import debugbreak from "debugbreak";
|
|
5
|
+
import { measureWrap } from "./profiling/measure";
|
|
6
|
+
|
|
7
|
+
let classes: {
|
|
8
|
+
[classGuid: string]: {
|
|
9
|
+
controller: SocketExposedInterface;
|
|
10
|
+
shape: SocketExposedShape;
|
|
11
|
+
}
|
|
12
|
+
} = {};
|
|
13
|
+
let exposedClasses = new Set<string>();
|
|
14
|
+
|
|
15
|
+
let globalHooks: SocketFunctionHook[] = [];
|
|
16
|
+
let globalClientHooks: SocketFunctionClientHook[] = [];
|
|
17
|
+
|
|
18
|
+
export async function performLocalCall(
|
|
19
|
+
config: {
|
|
20
|
+
call: FullCallType;
|
|
21
|
+
caller: CallerContext;
|
|
22
|
+
}
|
|
23
|
+
): Promise<unknown> {
|
|
24
|
+
const { call, caller } = config;
|
|
25
|
+
let classDef = classes[call.classGuid];
|
|
26
|
+
|
|
27
|
+
if (!classDef) {
|
|
28
|
+
throw new Error(`Class ${call.classGuid} not found`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!exposedClasses.has(call.classGuid)) {
|
|
32
|
+
throw new Error(`Class ${call.classGuid} not exposed`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let controller = classDef.controller;
|
|
36
|
+
let functionShape = classDef.shape[call.functionName];
|
|
37
|
+
if (!functionShape) {
|
|
38
|
+
throw new Error(`Function ${call.functionName} not exposed`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!controller[call.functionName]) {
|
|
42
|
+
throw new Error(`Function ${call.functionName} does not exist`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let serverContext = await runServerHooks(call, caller, functionShape);
|
|
46
|
+
if ("overrideResult" in serverContext) {
|
|
47
|
+
return serverContext.overrideResult;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// NOTE: We purposely don't await inside _setSocketContext, so the context is reset synchronously
|
|
51
|
+
let result = _setSocketContext(caller, () => {
|
|
52
|
+
return controller[call.functionName](...call.args);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return await result;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function isDataImmutable(call: CallType) {
|
|
59
|
+
return !!classes[call.classGuid]?.shape[call.functionName]?.dataImmutable;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function registerClass(classGuid: string, controller: SocketExposedInterface, shape: SocketExposedShape) {
|
|
63
|
+
if (classes[classGuid]) {
|
|
64
|
+
throw new Error(`Class ${classGuid} already registered`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
classes[classGuid] = {
|
|
68
|
+
controller,
|
|
69
|
+
shape,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function exposeClass(exposedClass: SocketRegistered) {
|
|
74
|
+
exposedClasses.add(exposedClass._classGuid);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function registerGlobalHook(hook: SocketFunctionHook) {
|
|
78
|
+
globalHooks.push(hook);
|
|
79
|
+
}
|
|
80
|
+
export function unregisterGlobalHook(hook: SocketFunctionHook) {
|
|
81
|
+
let index = globalHooks.indexOf(hook);
|
|
82
|
+
if (index >= 0) {
|
|
83
|
+
globalHooks.splice(index, 1);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export function registerGlobalClientHook(hook: SocketFunctionClientHook) {
|
|
87
|
+
globalClientHooks.push(hook);
|
|
88
|
+
}
|
|
89
|
+
export function unregisterGlobalClientHook(hook: SocketFunctionClientHook) {
|
|
90
|
+
let index = globalClientHooks.indexOf(hook);
|
|
91
|
+
if (index >= 0) {
|
|
92
|
+
globalClientHooks.splice(index, 1);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export const runClientHooks = measureWrap(async function runClientHooks(
|
|
97
|
+
callType: FullCallType,
|
|
98
|
+
hooks: SocketExposedShape[""],
|
|
99
|
+
connectionId: { nodeId: string },
|
|
100
|
+
): Promise<ClientHookContext> {
|
|
101
|
+
let context: ClientHookContext = { call: callType, connectionId };
|
|
102
|
+
|
|
103
|
+
let clientHooks = (
|
|
104
|
+
globalClientHooks
|
|
105
|
+
.concat(hooks.clientHooks || [])
|
|
106
|
+
);
|
|
107
|
+
for (let otherClientHook of globalHooks.concat(hooks.hooks || []).map(x => x.clientHook)) {
|
|
108
|
+
if (otherClientHook) {
|
|
109
|
+
clientHooks.push(otherClientHook);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
for (let hook of clientHooks) {
|
|
113
|
+
await hook(context);
|
|
114
|
+
if ("overrideResult" in context) {
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return context;
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
export const runServerHooks = measureWrap(async function runServerHooks(
|
|
123
|
+
callType: FullCallType,
|
|
124
|
+
caller: CallerContext,
|
|
125
|
+
hooks: SocketExposedShape[""],
|
|
126
|
+
): Promise<HookContext> {
|
|
127
|
+
let hookContext: HookContext = { call: callType };
|
|
128
|
+
for (let hook of globalHooks.concat(hooks.hooks || [])) {
|
|
129
|
+
await _setSocketContext(caller, () => hook(hookContext));
|
|
130
|
+
if ("overrideResult" in hookContext) {
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return hookContext;
|
|
135
135
|
});
|
package/src/certStore.ts
CHANGED
|
@@ -1,30 +1,30 @@
|
|
|
1
|
-
import * as tls from "tls";
|
|
2
|
-
import { isNode, sha256Hash } from "./misc";
|
|
3
|
-
|
|
4
|
-
let trustedCerts = new Set<string>();
|
|
5
|
-
let watchCallbacks = new Set<(certs: string[]) => void>();
|
|
6
|
-
|
|
7
|
-
/** Must be populated before the server starts */
|
|
8
|
-
export function trustCertificate(cert: string | Buffer) {
|
|
9
|
-
cert = cert.toString();
|
|
10
|
-
if (trustedCerts.has(cert)) return;
|
|
11
|
-
trustedCerts.add(cert);
|
|
12
|
-
let certs = getTrustedCertificates();
|
|
13
|
-
for (let callback of watchCallbacks) {
|
|
14
|
-
callback(certs);
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
export function getTrustedCertificates(): string[] {
|
|
18
|
-
let certs: string[] = [];
|
|
19
|
-
if (isNode()) {
|
|
20
|
-
certs.push(...tls.rootCertificates);
|
|
21
|
-
}
|
|
22
|
-
certs.push(...Array.from(trustedCerts));
|
|
23
|
-
return certs;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function watchTrustedCertificates(callback: (certs: string[]) => void) {
|
|
27
|
-
watchCallbacks.add(callback);
|
|
28
|
-
callback(getTrustedCertificates());
|
|
29
|
-
return () => watchCallbacks.delete(callback);
|
|
1
|
+
import * as tls from "tls";
|
|
2
|
+
import { isNode, sha256Hash } from "./misc";
|
|
3
|
+
|
|
4
|
+
let trustedCerts = new Set<string>();
|
|
5
|
+
let watchCallbacks = new Set<(certs: string[]) => void>();
|
|
6
|
+
|
|
7
|
+
/** Must be populated before the server starts */
|
|
8
|
+
export function trustCertificate(cert: string | Buffer) {
|
|
9
|
+
cert = cert.toString();
|
|
10
|
+
if (trustedCerts.has(cert)) return;
|
|
11
|
+
trustedCerts.add(cert);
|
|
12
|
+
let certs = getTrustedCertificates();
|
|
13
|
+
for (let callback of watchCallbacks) {
|
|
14
|
+
callback(certs);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export function getTrustedCertificates(): string[] {
|
|
18
|
+
let certs: string[] = [];
|
|
19
|
+
if (isNode()) {
|
|
20
|
+
certs.push(...tls.rootCertificates);
|
|
21
|
+
}
|
|
22
|
+
certs.push(...Array.from(trustedCerts));
|
|
23
|
+
return certs;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function watchTrustedCertificates(callback: (certs: string[]) => void) {
|
|
27
|
+
watchCallbacks.add(callback);
|
|
28
|
+
callback(getTrustedCertificates());
|
|
29
|
+
return () => watchCallbacks.delete(callback);
|
|
30
30
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
const arrayIsSplitable = Symbol.for("arrayIsSplitable");
|
|
2
|
-
export function markArrayAsSplitable<T>(data: T[]): T[] {
|
|
3
|
-
(data as any)[arrayIsSplitable] = true;
|
|
4
|
-
return data;
|
|
5
|
-
}
|
|
6
|
-
export function isSplitableArray<T>(data: T): data is T & (unknown[]) {
|
|
7
|
-
if (!Array.isArray(data)) return false;
|
|
8
|
-
return !!(data as any)[arrayIsSplitable];
|
|
1
|
+
const arrayIsSplitable = Symbol.for("arrayIsSplitable");
|
|
2
|
+
export function markArrayAsSplitable<T>(data: T[]): T[] {
|
|
3
|
+
(data as any)[arrayIsSplitable] = true;
|
|
4
|
+
return data;
|
|
5
|
+
}
|
|
6
|
+
export function isSplitableArray<T>(data: T): data is T & (unknown[]) {
|
|
7
|
+
if (!Array.isArray(data)) return false;
|
|
8
|
+
return !!(data as any)[arrayIsSplitable];
|
|
9
9
|
}
|
package/src/formatting/colors.ts
CHANGED
|
@@ -1,79 +1,79 @@
|
|
|
1
|
-
export type HSL = { h: number, s: number, l: number };
|
|
2
|
-
export function hslText(color: HSL): string {
|
|
3
|
-
return `hsl(${color.h}, ${color.s}%, ${color.l}%)`;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
export function hslToRGB(color: HSL) {
|
|
7
|
-
let { h, s, l } = color;
|
|
8
|
-
h /= 360;
|
|
9
|
-
s /= 100;
|
|
10
|
-
l /= 100;
|
|
11
|
-
let r, g, b;
|
|
12
|
-
if (s === 0) {
|
|
13
|
-
r = g = b = l; // achromatic
|
|
14
|
-
} else {
|
|
15
|
-
const hue2rgb = (p: number, q: number, t: number) => {
|
|
16
|
-
if (t < 0) t += 1;
|
|
17
|
-
if (t > 1) t -= 1;
|
|
18
|
-
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
|
19
|
-
if (t < 1 / 2) return q;
|
|
20
|
-
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
|
21
|
-
return p;
|
|
22
|
-
};
|
|
23
|
-
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
24
|
-
const p = 2 * l - q;
|
|
25
|
-
r = hue2rgb(p, q, h + 1 / 3);
|
|
26
|
-
g = hue2rgb(p, q, h);
|
|
27
|
-
b = hue2rgb(p, q, h - 1 / 3);
|
|
28
|
-
if (b < 0) {
|
|
29
|
-
b = 0;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
return { r: Math.floor(r * 255), g: Math.floor(g * 255), b: Math.floor(b * 255) };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function hslToHex(color: HSL) {
|
|
36
|
-
let { r, g, b } = hslToRGB(color);
|
|
37
|
-
const toHex = (x: number) => {
|
|
38
|
-
const hex = x.toString(16);
|
|
39
|
-
return hex.length === 1 ? "0" + hex : hex;
|
|
40
|
-
};
|
|
41
|
-
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Consider
|
|
45
|
-
export function hslLightenGamma(hsl: HSL, fraction: number) {
|
|
46
|
-
let l = ((hsl.l ** 2) * (1 + fraction)) ** 0.5;
|
|
47
|
-
if (l > 255) l = 255;
|
|
48
|
-
if (!l || l < 0) l = 0;
|
|
49
|
-
return { h: hsl.h, s: hsl.s, l };
|
|
50
|
-
}
|
|
51
|
-
export function hslLightenLinear(hsl: HSL, lightness: number) {
|
|
52
|
-
let l = hsl.l + lightness;
|
|
53
|
-
if (l > 255) l = 255;
|
|
54
|
-
if (!l || l < 0) l = 0;
|
|
55
|
-
return { h: hsl.h, s: hsl.s, l };
|
|
56
|
-
}
|
|
57
|
-
export function hslDarkenGamma(hsl: HSL, fraction: number) {
|
|
58
|
-
return hslLightenGamma(hsl, -fraction);
|
|
59
|
-
}
|
|
60
|
-
export function hslDarkenLinear(hsl: HSL, lightness: number) {
|
|
61
|
-
return hslLightenLinear(hsl, -lightness);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function hslAddSaturate(hsl: HSL, saturation: number) {
|
|
65
|
-
// If something has ZERO saturation, the color is meaningless, so increasing the saturation
|
|
66
|
-
// will likely not give the desired impact. Instead, darken the grey, which should give the desire effect.
|
|
67
|
-
if (hsl.s === 0) {
|
|
68
|
-
return hslDarkenGamma(hsl, 0.2);
|
|
69
|
-
}
|
|
70
|
-
return { h: hsl.h, s: hsl.s + saturation, l: hsl.l };
|
|
71
|
-
}
|
|
72
|
-
export function hslSetSaturate(hsl: HSL, saturation: number) {
|
|
73
|
-
// If something has ZERO saturation, the color is meaningless, so increasing the saturation
|
|
74
|
-
// will likely not give the desired impact. Instead, set the lightness.
|
|
75
|
-
if (hsl.s === 0) {
|
|
76
|
-
return { h: hsl.h, s: hsl.s, l: 100 - saturation };
|
|
77
|
-
}
|
|
78
|
-
return { h: hsl.h, s: saturation, l: hsl.l };
|
|
1
|
+
export type HSL = { h: number, s: number, l: number };
|
|
2
|
+
export function hslText(color: HSL): string {
|
|
3
|
+
return `hsl(${color.h}, ${color.s}%, ${color.l}%)`;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function hslToRGB(color: HSL) {
|
|
7
|
+
let { h, s, l } = color;
|
|
8
|
+
h /= 360;
|
|
9
|
+
s /= 100;
|
|
10
|
+
l /= 100;
|
|
11
|
+
let r, g, b;
|
|
12
|
+
if (s === 0) {
|
|
13
|
+
r = g = b = l; // achromatic
|
|
14
|
+
} else {
|
|
15
|
+
const hue2rgb = (p: number, q: number, t: number) => {
|
|
16
|
+
if (t < 0) t += 1;
|
|
17
|
+
if (t > 1) t -= 1;
|
|
18
|
+
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
|
19
|
+
if (t < 1 / 2) return q;
|
|
20
|
+
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
|
21
|
+
return p;
|
|
22
|
+
};
|
|
23
|
+
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
24
|
+
const p = 2 * l - q;
|
|
25
|
+
r = hue2rgb(p, q, h + 1 / 3);
|
|
26
|
+
g = hue2rgb(p, q, h);
|
|
27
|
+
b = hue2rgb(p, q, h - 1 / 3);
|
|
28
|
+
if (b < 0) {
|
|
29
|
+
b = 0;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return { r: Math.floor(r * 255), g: Math.floor(g * 255), b: Math.floor(b * 255) };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function hslToHex(color: HSL) {
|
|
36
|
+
let { r, g, b } = hslToRGB(color);
|
|
37
|
+
const toHex = (x: number) => {
|
|
38
|
+
const hex = x.toString(16);
|
|
39
|
+
return hex.length === 1 ? "0" + hex : hex;
|
|
40
|
+
};
|
|
41
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Consider
|
|
45
|
+
export function hslLightenGamma(hsl: HSL, fraction: number) {
|
|
46
|
+
let l = ((hsl.l ** 2) * (1 + fraction)) ** 0.5;
|
|
47
|
+
if (l > 255) l = 255;
|
|
48
|
+
if (!l || l < 0) l = 0;
|
|
49
|
+
return { h: hsl.h, s: hsl.s, l };
|
|
50
|
+
}
|
|
51
|
+
export function hslLightenLinear(hsl: HSL, lightness: number) {
|
|
52
|
+
let l = hsl.l + lightness;
|
|
53
|
+
if (l > 255) l = 255;
|
|
54
|
+
if (!l || l < 0) l = 0;
|
|
55
|
+
return { h: hsl.h, s: hsl.s, l };
|
|
56
|
+
}
|
|
57
|
+
export function hslDarkenGamma(hsl: HSL, fraction: number) {
|
|
58
|
+
return hslLightenGamma(hsl, -fraction);
|
|
59
|
+
}
|
|
60
|
+
export function hslDarkenLinear(hsl: HSL, lightness: number) {
|
|
61
|
+
return hslLightenLinear(hsl, -lightness);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function hslAddSaturate(hsl: HSL, saturation: number) {
|
|
65
|
+
// If something has ZERO saturation, the color is meaningless, so increasing the saturation
|
|
66
|
+
// will likely not give the desired impact. Instead, darken the grey, which should give the desire effect.
|
|
67
|
+
if (hsl.s === 0) {
|
|
68
|
+
return hslDarkenGamma(hsl, 0.2);
|
|
69
|
+
}
|
|
70
|
+
return { h: hsl.h, s: hsl.s + saturation, l: hsl.l };
|
|
71
|
+
}
|
|
72
|
+
export function hslSetSaturate(hsl: HSL, saturation: number) {
|
|
73
|
+
// If something has ZERO saturation, the color is meaningless, so increasing the saturation
|
|
74
|
+
// will likely not give the desired impact. Instead, set the lightness.
|
|
75
|
+
if (hsl.s === 0) {
|
|
76
|
+
return { h: hsl.h, s: hsl.s, l: 100 - saturation };
|
|
77
|
+
}
|
|
78
|
+
return { h: hsl.h, s: saturation, l: hsl.l };
|
|
79
79
|
}
|