-
Notifications
You must be signed in to change notification settings - Fork 19
/
color.ts
126 lines (96 loc) · 3.07 KB
/
color.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
type Args = {
amount: number;
format: string;
group: number;
sample: number;
}
type Data = Uint8ClampedArray
type Handler = (data: Data, args: Args) => Output
type Hex = string
type Input = (Hex | Rgb)[]
type Item = Url | HTMLImageElement
type Output = Hex | Rgb | (Hex | Rgb)[]
type Rgb = number[]
type Url = string
const getSrc = (item: Item): string =>
typeof item === 'string' ? item : item.src
const getArgs = ({
amount = 3,
format = 'array',
group = 20,
sample = 10,
} = {}): Args => ({ amount, format, group, sample })
const format = (input: Input, args: Args): Output => {
const list = input.map((val) => {
const rgb = Array.isArray(val) ? val : val.split(',').map(Number)
return args.format === 'hex' ? rgbToHex(rgb) : rgb
})
return args.amount === 1 || list.length === 1 ? list[0] : list
}
const group = (number: number, grouping: number): number => {
const grouped = Math.round(number / grouping) * grouping
return Math.min(grouped, 255)
}
const rgbToHex = (rgb: Rgb): Hex => '#' + rgb.map((val) => {
const hex = val.toString(16)
return hex.length === 1 ? '0' + hex : hex
}).join('')
const getImageData = (src: Url): Promise<Data> =>
new Promise((resolve, reject) => {
const canvas = document.createElement('canvas')
const context = <CanvasRenderingContext2D>canvas.getContext('2d')
const img = new Image
img.onload = () => {
canvas.height = img.height
canvas.width = img.width
context.drawImage(img, 0, 0)
const data = context.getImageData(0, 0, img.width, img.height).data
resolve(data)
}
img.onerror = () => reject(Error('Image loading failed.'))
img.crossOrigin = ''
img.src = src
})
const getAverage = (data: Data, args: Args): Output => {
const gap = 4 * args.sample
const amount = data.length / gap
const rgb = { r: 0, g: 0, b: 0 }
for (let i = 0; i < data.length; i += gap) {
rgb.r += data[i]
rgb.g += data[i + 1]
rgb.b += data[i + 2]
}
return format([[
Math.round(rgb.r / amount),
Math.round(rgb.g / amount),
Math.round(rgb.b / amount)
]], args)
}
const getProminent = (data: Data, args: Args): Output => {
const gap = 4 * args.sample
const colors: { [key: string]: number } = {}
for (let i = 0; i < data.length; i += gap) {
const rgb = [
group(data[i], args.group),
group(data[i + 1], args.group),
group(data[i + 2], args.group),
].join()
colors[rgb] = colors[rgb] ? colors[rgb] + 1 : 1
}
return format(
Object.entries(colors)
.sort(([_keyA, valA], [_keyB, valB]) => valA > valB ? -1 : 1)
.slice(0, args.amount)
.map(([rgb]) => rgb),
args
)
}
const process = (item: Item, args: Args, handler: Handler): Promise<Output> =>
new Promise((resolve, reject) =>
getImageData(getSrc(item))
.then((data) => resolve(handler(data, getArgs(args))))
.catch((error) => reject(error))
)
const average = (item: Item, args: Args) => process(item, args, getAverage)
const prominent = (item: Item, args: Args) => process(item, args, getProminent)
export { average, prominent }