UNPKG

12 kBJavaScriptView Raw
1/*
2 Terminal Kit
3
4 Copyright (c) 2009 - 2020 Cédric Ronvel
5
6 The MIT License (MIT)
7
8 Permission is hereby granted, free of charge, to any person obtaining a copy
9 of this software and associated documentation files (the "Software"), to deal
10 in the Software without restriction, including without limitation the rights
11 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 copies of the Software, and to permit persons to whom the Software is
13 furnished to do so, subject to the following conditions:
14
15 The above copyright notice and this permission notice shall be included in all
16 copies or substantial portions of the Software.
17
18 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24 SOFTWARE.
25*/
26
27"use strict" ;
28
29
30
31const termkit = require( './termkit.js' ) ;
32
33
34
35/*
36 Custom 256-colors palette for ScreenBuffer, each color is 24 bits.
37 Enhance ScreenBuffer without relying on ScreenBufferHD.
38
39 The original 6x6x6 colors cube shipped with terminal software is rather boring and miss the spot.
40 Lot of useless colors, lot of over-saturated hideous colors, and useful colors cannot be shaded/hilighted easily.
41
42 There are 4 parts :
43
44 * The first 16 colors are reserved, it always map to ANSI colors and depends on the terminal settings entirely.
45 * Then comes a 216-colors adaptive palette based upon 12 user-provided colors that represent 12 hue of the wheel,
46 with auto-generated variation of shades, tint and desaturation.
47 That is: 12 colors * 6 level of tint/shade * 3 level of desaturation = 216 colors.
48 For maximum reliance, there are computed using the HCL (Lab) colorspace.
49 This provide some good hilight and dim effect
50 * Then comes 13 extra colors fully defined by the user, they must be precise and beautiful colors that are missing
51 in the 216-colors part, and that have great added values.
52*/
53
54const defaultAdaptivePaletteDef = [
55 { names: [ 'red' ] , code: '#e32322' } ,
56 { names: [ 'orange' ] , code: '#f18e1c' } ,
57 { names: [ 'gold' , 'yellow-orange' , 'amber' ] , code: '#fdc60b' } ,
58 { names: [ 'yellow' ] , code: '#f4e500' } ,
59 { names: [ 'chartreuse' , 'yellow-green' ] , code: '#8cbb26' } ,
60 { names: [ 'green' ] , code: '#25ad28' } ,
61 { names: [ 'turquoise' , 'turquoise-green' ] , code: '#1bc17d' } ,
62 { names: [ 'cyan' , 'turquoise-blue' ] , code: '#0dc0cd' } ,
63 { names: [ 'blue' ] , code: '#2a60b0' } ,
64 { names: [ 'indigo' ] , code: '#3b3ba2' } ,
65 { names: [ 'violet' , 'purple' ] , code: '#713795' } ,
66 { names: [ 'magenta' ] , code: '#bd0a7d' }
67] ;
68
69
70
71// 13 extra colors
72const defaultExtraPaletteDef = [
73 { names: [ 'crimson' ] , code: '#dc143c' } ,
74 { names: [ 'vermilion' , 'cinnabar' ] , code: '#e34234' } ,
75 { names: [ 'brown' ] , code: '#a52a2a' } ,
76 { names: [ 'bronze' ] , code: '#cd7f32' } ,
77 { names: [ 'coquelicot' ] , code: '#ff3800' } ,
78 //{ names: [ 'flame' ] , code: '#e25822' } ,
79 //{ names: [ 'salmon' ] , code: '#ff8c69' } ,
80 { names: [ 'coral-pink' ] , code: '#f88379' } ,
81 { names: [ 'see-green' ] , code: '#2e8b57' } ,
82 { names: [ 'medium-spring-green' ] , code: '#00fa9a' } ,
83 { names: [ 'olivine' ] , code: '#9ab973' } ,
84 { names: [ 'royal-blue' ] , code: '#4169e1' } ,
85 { names: [ 'purple' ] , code: '#800080' } ,
86 //{ names: [ 'tyrian-purple' ] , code: '#66023c' } ,
87 //{ names: [ 'purple-heart' ] , code: '#69359c' } ,
88 { names: [ 'lavender-purple' ] , code: '#967bb6' } ,
89 //{ names: [ 'classic-rose' , 'light-pink' ] , code: '#fbcce7' } ,
90 { names: [ 'pink' ] , code: '#ffc0cb' }
91 //{ names: [ 'lime' , 'lemon-lime' ] , code: '#bfff00' } ,
92] ;
93
94
95
96const ansiColorIndex = {
97 black: 0 ,
98 red: 1 ,
99 green: 2 ,
100 yellow: 3 ,
101 blue: 4 ,
102 magenta: 5 ,
103 violet: 5 ,
104 cyan: 6 ,
105 white: 7 ,
106 grey: 8 ,
107 gray: 8 ,
108 'bright-black': 8 ,
109 'bright-red': 9 ,
110 'bright-green': 10 ,
111 'bright-yellow': 11 ,
112 'bright-blue': 12 ,
113 'bright-magenta': 13 ,
114 'bright-violet': 13 ,
115 'bright-cyan': 14 ,
116 'bright-white': 15
117} ;
118
119
120function Palette( options = {} ) {
121 this.term = options.term || termkit.terminal ;
122 this.system = !! options.system ;
123 this.adaptivePaletteDef = this.system ? null : options.adaptivePaletteDef || defaultAdaptivePaletteDef ;
124 this.extraPaletteDef = this.system ? null : options.extraPaletteDef || defaultExtraPaletteDef ;
125 this.escape = [] ;
126 this.bgEscape = [] ;
127 this.chromaColors = [] ;
128 this.colorIndex = {} ;
129
130 // Because that function is often passed as argument... easier to bind it here once for all
131 this.colorNameToIndex = this.colorNameToIndex.bind( this ) ;
132
133 this.generate() ;
134}
135
136module.exports = Palette ;
137
138
139
140Palette.prototype.colorNameToIndex = function( name ) {
141 name = name.toLowerCase() ;
142 return this.colorIndex[ name ] || termkit.colorNameToIndex( name ) ;
143} ;
144
145
146
147Palette.prototype.generate = function() {
148 this.generateDefaultMapping() ;
149 this.generateAnsiColorNames() ;
150 this.generateAdaptive() ;
151 this.generateExtra() ;
152 this.generateGrayscale() ;
153} ;
154
155
156
157// It just generates default terminal mapping for 256 colors
158Palette.prototype.generateDefaultMapping = function() {
159 var register ;
160
161 for ( register = 0 ; register < 256 ; register ++ ) {
162 this.escape[ register ] = this.term.str.color256( register ) ;
163 this.bgEscape[ register ] = this.term.str.bgColor256( register ) ;
164 }
165} ;
166
167
168
169// It just generates default terminal mapping for 256 colors
170Palette.prototype.generateAnsiColorNames = function() {
171 var name , strippedName ;
172
173 for ( name in ansiColorIndex ) {
174 strippedName = name.replace( /-/g , '' ) ;
175 this.colorIndex[ name ] = ansiColorIndex[ name ] ;
176
177 if ( strippedName !== name ) {
178 this.colorIndex[ strippedName ] = ansiColorIndex[ name ] ;
179 }
180 }
181} ;
182
183
184
185Palette.prototype.generateAdaptive = function() {
186 if ( this.system ) { return ; }
187
188 var i , j , z , register ,
189 baseChromaColors , chromaColor ,
190 saturationMark , lightnessMark , suffix ;
191
192 baseChromaColors = this.adaptivePaletteDef.map( color => termkit.chroma( color.code ) ) ;
193
194 register = 16 ;
195
196 for ( z = 0 ; z >= -2 ; z -- ) {
197 if ( z > 0 ) {
198 saturationMark = '!'.repeat( z ) ;
199 }
200 else if ( z < 0 ) {
201 saturationMark = '~'.repeat( -z ) ;
202 }
203 else {
204 saturationMark = '' ;
205 }
206
207 for ( j = 2 ; j >= -3 ; j -- ) {
208
209 if ( j > 0 ) {
210 lightnessMark = '+'.repeat( j ) ;
211 }
212 else if ( j < 0 ) {
213 lightnessMark = '-'.repeat( -j ) ;
214 }
215 else {
216 lightnessMark = '' ;
217 }
218
219 suffix = saturationMark + lightnessMark ;
220
221 for ( i = 0 ; i < 12 ; i ++ ) {
222 chromaColor = this.clStep( baseChromaColors[ i ] , z , j ) ;
223 this.addColor( register , chromaColor , this.adaptivePaletteDef[ i ].names , '@' , suffix ) ;
224 register ++ ;
225 }
226 }
227
228 }
229} ;
230
231
232
233Palette.prototype.generateExtra = function() {
234 if ( this.system ) { return ; }
235
236 var i , register ;
237
238 register = 232 ;
239
240 for ( i = 0 ; i < 13 && i < this.extraPaletteDef.length ; i ++ ) {
241 this.addColor( register , termkit.chroma( this.extraPaletteDef[ i ].code ) , this.extraPaletteDef[ i ].names , '*' ) ;
242 register ++ ;
243 }
244} ;
245
246
247
248const grayscaleNames = [
249 [ 'black' ] ,
250 [ 'darkest-gray' ] ,
251 [ 'darker-gray' ] ,
252 [ 'dark-gray' ] ,
253 [ 'dark-medium-gray' ] ,
254 [ 'medium-gray' , 'gray' ] ,
255 [ 'light-medium-gray' ] ,
256 [ 'light-gray' ] ,
257 [ 'lighter-gray' ] ,
258 [ 'lightest-gray' ] ,
259 [ 'white' ]
260] ;
261
262Palette.prototype.generateGrayscale = function() {
263 if ( this.system ) { return ; }
264
265 var i , register , chromaColor ;
266
267 register = 245 ;
268
269 for ( i = 0 ; i <= 10 ; i ++ ) {
270 chromaColor = termkit.chroma( 0 , 0 , 10 * i , 'hcl' ) ;
271 this.addColor( register , chromaColor , grayscaleNames[ i ] , '@' ) ;
272 register ++ ;
273 }
274} ;
275
276
277
278Palette.prototype.getRgb = function( register ) {
279 var chromaColor = this.chromaColors[ register ] ;
280 if ( ! chromaColor ) { return null ; }
281 var [ r , g , b ] = chromaColor.rgb() ;
282 return { r , g , b } ;
283} ;
284
285
286
287Palette.prototype.addColor = function( register , chromaColor , names , prefix = '' , suffix = '' ) {
288 var targetRegister ,
289 [ r , g , b ] = chromaColor.rgb() ;
290
291 this.chromaColors[ register ] = chromaColor ;
292
293 if ( this.term.support.trueColor ) {
294 this.escape[ register ] = this.term.str.colorRgb( r , g , b ) ;
295 this.bgEscape[ register ] = this.term.str.bgColorRgb( r , g , b ) ;
296 }
297 else if ( this.term.support['256colors'] ) {
298 targetRegister = this.term.registerForRgb(
299 { r , g , b } ,
300 r === g && g === b ? 232 : 0 , // minRegister is the start of the grayscale if r=g=b
301 255
302 ) ;
303
304 this.escape[ register ] = this.term.str.color256( targetRegister ) ;
305 this.bgEscape[ register ] = this.term.str.bgColor256( targetRegister ) ;
306 }
307 else {
308 targetRegister = this.term.registerForRgb( { r , g , b } , 0 , 15 ) ;
309 this.escape[ register ] = this.term.str.color256( targetRegister ) ;
310 this.bgEscape[ register ] = this.term.str.bgColor256( targetRegister ) ;
311 }
312
313 names.forEach( name => {
314 var strippedName = prefix + name.replace( /-/g , '' ) + suffix ;
315 name = prefix + name + suffix ;
316 this.colorIndex[ name ] = register ;
317
318 if ( strippedName !== name ) {
319 this.colorIndex[ strippedName ] = register ;
320 }
321 } ) ;
322} ;
323
324
325
326const FIX_STEP = 1.1 ;
327
328Palette.prototype.clStep = function( chromaColor , cAdjust , lAdjust , fixRgb = true ) {
329 var c , l , rgb , avg , sortedChannels , preserveLOverC ;
330
331 if ( ! cAdjust && ! lAdjust ) { return chromaColor ; }
332
333 c = chromaColor.get( 'hcl.c' ) ;
334 l = chromaColor.get( 'hcl.l' ) ;
335
336 /*
337 c += c * cAdjust / 3 ;
338 l += l * lAdjust / 4 ;
339 //*/
340
341 c *= ( cAdjust > 0 ? 1.6 : 1.7 ) ** cAdjust ;
342 l *= ( lAdjust > 0 ? 1.2 : 1.35 ) ** lAdjust ;
343
344 chromaColor = chromaColor.set( 'hcl.c' , c ).set( 'hcl.l' , l ) ;
345
346 if ( ! fixRgb || ! chromaColor.clipped ) { return chromaColor ; }
347
348 // RGB is clipped and should be fixed
349 // The most critical part is when the hue get changed, since it's arguably the most important information
350 // Lightness is somewhat important too, but less than hue a bit more than the Chroma
351 // Chroma will be preserved if the adjustement is greater on it than on lightness
352
353 //preserveLOverC = Math.abs( lAdjust ) >= Math.abs( cAdjust ) ;
354 preserveLOverC = Math.abs( lAdjust ) >= cAdjust ;
355
356 for ( ;; ) {
357 // chromaColor.clipped is not reliable since integer rounding counts as clipping...
358 rgb = chromaColor._rgb._unclipped ;
359 rgb.length = 3 ;
360
361 if ( rgb.every( channel => channel > -5 && channel < 260 ) ) { return chromaColor ; }
362
363 sortedChannels = [ ... rgb ].sort() ;
364
365 //console.log( "Clipped!" , rgb , chromaColor.rgb() ) ;
366
367 if ( sortedChannels[ 2 ] >= 256 ) {
368 // Clipping will affect hue!
369 avg = ( sortedChannels[ 0 ] + sortedChannels[ 1 ] + sortedChannels[ 2 ] ) / 3 ;
370
371 if ( preserveLOverC ) {
372 // Desaturate a bit and retry
373 c = chromaColor.get( 'hcl.c' ) ;
374 c /= FIX_STEP ;
375 chromaColor = chromaColor.set( 'hcl.c' , c ) ;
376 }
377 else {
378 // Darken a bit and retry
379 l = chromaColor.get( 'hcl.l' ) ;
380 l /= FIX_STEP ;
381 chromaColor = chromaColor.set( 'hcl.l' , l ) ;
382 }
383
384 // It was too bright anyway, let it be clipped
385 if ( avg > 255 ) { return chromaColor ; }
386 }
387 else if ( sortedChannels[ 1 ] < 0 ) {
388 // Clipping will affect hue!
389 avg = ( sortedChannels[ 0 ] + sortedChannels[ 1 ] + sortedChannels[ 2 ] ) / 3 ;
390
391 if ( preserveLOverC ) {
392 // Desaturate a bit and retry
393 c = chromaColor.get( 'hcl.c' ) ;
394 c /= FIX_STEP ;
395 chromaColor = chromaColor.set( 'hcl.c' , c ) ;
396 }
397 else {
398 // Lighten a bit and retry
399 l = chromaColor.get( 'hcl.l' ) ;
400 l *= FIX_STEP ;
401 chromaColor = chromaColor.set( 'hcl.l' , l ) ;
402 }
403
404 // It was too dark anyway, let it be clipped
405 if ( avg < 0 ) { return chromaColor ; }
406 }
407 else {
408 // This clipping (lowest channel below 0) will not affect hue, only lightness, let it be clipped
409 return chromaColor ;
410 }
411 }
412} ;
413