agentscript
Version:
AgentScript Model in Model/View architecture
345 lines (315 loc) • 11.4 kB
JavaScript
import * as util from './utils.js'
// /** @module */
/**
*
* A general color module, supporting css string colors, canvas2d pixel
* colors, webgl and canvas2d Uint8ClampedArray r,g,b,a arrays.
*
* #### CSS Color Strings.
*
* CSS colors in HTML are strings, see [Mozillas Color Reference](
* https://developer.mozilla.org/en-US/docs/Web/CSS/color_value),
* taking one of 7 forms:
*
* - Names: over 140 color case-insensitive names like
* Red, Green, CadetBlue, etc.
* - Hex, short and long form: #0f0, #ff10a0
* - RGB: rgb(255, 0, 0), rgba(255, 0, 0, 0.5)
* - HSL: hsl(120, 100%, 50%), hsla(120, 100%, 50%, 0.8)
*
* See [this wikipedia article](https://goo.gl/ev8Kw0)
* on differences between HSL and HSB/HSV.
*
*/
/** @namespace */
const Color = {
/**
* Convert 4 r,g,b,a ints in [0-255] ("a" defaulted to 255) to a
* css color string.
*
* @param {number} r integer in [0, 255] for red channel
* @param {number} g integer in [0, 255] for green channel
* @param {number} b integer in [0, 255] for blue channel
* @param {number} [a=255] integer in [0, 255] for alpha/opacity channel
* @returns {string} A rgb(r,g,b) or rgba(r,g,b,a) css color string
*/
rgbaCssColor(r, g, b, a = 255) {
a = a / 255
const a2 = a.toPrecision(2)
return a === 1 ? `rgb(${r},${g},${b})` : `rgba(${r},${g},${b},${a2})`
},
/**
* Convert 4 ints, h,s,l,a, h in [0-360], s,l in [0-100]% a in [0-255] to a
* css color string.
* See [hsl()](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/hsl).
*
* NOTE: h=0 and h=360 are the same, use h in 0-359 for unique colors.
*
* @param {number} h
* @param {number} [s=100]
* @param {number} [l=50]
* @param {number} [a=255]
* @returns {string} A css HSL color string
*/
hslCssColor(h, s = 100, l = 50, a = 255) {
a = a / 255
const a4 = a.toPrecision(4)
return a === 1
? `hsl(${h},${s}%,${l}%)`
: `hsla(${h},${s}%,${l}%,${a4})`
},
/**
* Return a html/css hex color string for an r,g,b opaque color (a=255).
* Hex strings do not support alpha.
* Both #nnn and #nnnnnn forms supported.
* Default is to check for the short hex form.
* @param {number} r Integer value for red channel
* @param {number} g Integer value for green channel
* @param {number} b Integer value for blue channel
* @returns {string} A css hex color string #nnn or #nnnnnn, n in [0,F] hex
*/
hexCssColor(r, g, b) {
return `#${(0x1000000 | (b | (g << 8) | (r << 16)))
.toString(16)
.slice(-6)}`
},
// cssColor is a hybrid string and is our standard. It returns:
//
// * rgbaCssColor if a not 255 (i.e. not opaque)
// * hexCssColor otherwise
cssColor(r, g, b, a = 255) {
return a === 255
? this.hexCssColor(r, g, b)
: this.rgbaCssColor(r, g, b, a)
},
randomCssColor() {
const r255 = () => util.randomInt(256) // random int in [0,255]
return this.cssColor(r255(), r255(), r255())
},
randomGrayCssColor(min = 0, max = 255) {
const gray = util.randomInt2(min, max) // random int in [min,max]
return this.cssColor(gray, gray, gray)
},
// ### Pixels
cssToPixel(string) {
const rgba = this.cssToUint8Array(string)
return this.rgbaToPixel(...rgba)
},
rgbaToPixel(r, g, b, a = 255) {
const rgba = new Uint8Array([r, g, b, a])
const pixels = new Uint32Array(rgba.buffer)
return pixels[0]
},
randomPixel() {
const r255 = () => util.randomInt(256) // random int in [0,255]
return this.rgbaToPixel(r255(), r255(), r255())
},
randomGrayPixel(min = 0, max = 255) {
const gray = util.randomInt2(min, max) // random int in [min,max]
return this.rgbaToPixel(gray, gray, gray)
},
// ### CSS String Conversions
// Return 4 element array given any legal CSS string color.
//
// Because strings vary widely: CadetBlue, #0f0, rgb(255,0,0),
// hsl(120,100%,50%), we do not parse strings, instead we let
// the browser do our work: we fill a 1x1 canvas with the css string color,
// returning the r,g,b,a canvas ImageData TypedArray.
// The shared 1x1 canvas 2D context.
sharedCtx1x1: util.createCtx(1, 1, false, { willReadFrequently: true }),
// sharedCtx1x1: util.createCtx(1, 1),
// Convert any css string to 4 element Uint8ClampedArray TypedArray.
// If you need a JavaScript Array, use `new Array(...TypedArray)`
// Slow, but works for all css strings: hsl, rgb, .. as well as names.
cssToUint8Array(string) {
this.sharedCtx1x1.clearRect(0, 0, 1, 1)
this.sharedCtx1x1.fillStyle = string
this.sharedCtx1x1.fillRect(0, 0, 1, 1)
return this.sharedCtx1x1.getImageData(0, 0, 1, 1).data
},
// ### Typed Color
// A TypedColor is a 4 element ArrayBuffer, with two views:
//
// * pixelArray: A single element Uint32Array view
// * u8array: A 4 element r,g,b,a Uint8ClampedArray view
//
// getters/setters are provided for multiple other color types:
// 'css', 'pixel', 'rgb', 'webgl'
// If g is undefinec, returns toTypedColor(g)
typedColor(r, g, b, a = 255) {
if (g === undefined) return this.toTypedColor(r)
const u8array = new Uint8ClampedArray([r, g, b, a])
u8array.pixelArray = new Uint32Array(u8array.buffer) // one element array
// Make this an instance of TypedColorProto
Object.setPrototypeOf(u8array, TypedColorProto)
return u8array
},
isTypedColor(any) {
return any && any.constructor === Uint8ClampedArray && any.pixelArray
},
// Return a typedColor given a value and optional colorType
// If the value already is a typedColor, simply return it
// If colorType not defined, assume css (string) or pixel (number)
// or array [r,g,b,a=255]
// The colorType can be: 'css', 'pixel', 'rgb', 'webgl'
// Note rgb and webgl are int arrays & float arrays respectively.
toTypedColor(value, colorType) {
if (this.isTypedColor(value)) return value
const tc = this.typedColor(0, 0, 0, 0) // "empty" typed color
if (colorType == null) {
if (util.isString(value)) tc.css = value
else if (util.isNumber(value)) tc.pixel = value
else if (util.isArray(value)) tc.rgb = value
else if (util.isTypedArray(value)) tc.rgb = value
else throw Error(`toTypedColor: illegal value ${value}`)
} else {
// REMIND: type check value & colorType?
tc[colorType] = value
}
return tc
},
// Random typedColor, rgb or gray, alpha = 255 for both:
randomTypedColor() {
const r255 = () => util.randomInt(256) // random int in [0,255]
return this.typedColor(r255(), r255(), r255())
},
// Random gray color, alpha = 255
randomGrayTypedColor(min = 0, max = 255) {
const gray = util.randomInt2(min, max) // random int in [min,max]
return this.typedColor(gray, gray, gray)
},
// Arrays of random typed colors, very useful for patch colors
randomColorArray(length) {
const colors = new Array(length)
util.forLoop(colors, (c, i) => (colors[i] = this.randomTypedColor()))
return colors
},
randomGrayArray(length, min = 0, max = 255) {
const grays = new Array(length)
util.forLoop(
grays,
(g, i) => (grays[i] = this.randomGrayTypedColor(min, max))
)
return grays
},
}
// Prototype for Color. Getters/setters for usability, may be slower.
const TypedColorProto = {
// Inherit from Uint8ClampedArray
__proto__: Uint8ClampedArray.prototype,
// Set the Color to new rgba values.
setColor(r, g, b, a = 255) {
this.checkColorChange()
this[0] = r
this[1] = g
this[2] = b
this[3] = a
},
// No real need for getColor, it *is* the typed Uint8 array
set rgb(rgbaArray) {
this.setColor(...rgbaArray)
},
get rgb() {
return this
},
// Set opacity to a value in 0-255
setAlpha(alpha) {
this.checkColorChange()
this[3] = alpha // Uint8ClampedArray will clamp to 0-255
},
getAlpha() {
return this[3]
},
get alpha() {
return this.getAlpha()
},
set alpha(alpha) {
this.setAlpha(alpha)
},
// Set the Color to a new pixel value
setPixel(pixel) {
this.checkColorChange()
this.pixelArray[0] = pixel
},
// Get the pixel value
getPixel() {
return this.pixelArray[0]
},
get pixel() {
return this.getPixel()
},
set pixel(pixel) {
this.setPixel(pixel)
},
// Set pixel/rgba values to equivalent of the css string.
// 'red', '#f00', 'ff0000', 'rgb(255,0,0)', etc.
//
// Does *not* set the chached this.string, which will be lazily evaluated
// to its common cssColor by getCss(). The above would all return '#f00'.
setCss(string) {
return this.setColor(...Color.cssToUint8Array(string))
},
// Return the cssColor for this Color, cached in the @string value
getCss() {
if (this.string == null) this.string = Color.cssColor(...this)
return this.string
},
get css() {
return this.getCss()
},
set css(string) {
this.setCss(string)
},
// Note: webgl colors are 3 RGB floats (no A)
setWebgl(array) {
if (array.length !== 3)
throw Error(
'setWebgl array length must be 3, length:',
array.length
)
this.setColor(
// OK if float * 255 non-int, setColor stores into uint8 array
array[0] * 255,
array[1] * 255,
array[2] * 255
)
},
getWebgl() {
return [this[0] / 255, this[1] / 255, this[2] / 255]
},
get webgl() {
return this.getWebgl()
},
set webgl(array) {
this.setWebgl(array)
},
// Housekeeping when the color is modified.
checkColorChange() {
// Reset string & webgl on color change.
this.string = null // will be lazy evaluated via getCss.
// this.floatArray = null
},
// Return true if color is same value as myself, comparing pixels
equals(color) {
return this.getPixel() === color.getPixel()
},
toString() {
return `[${Array.from(this).toString()}]`
},
// Return a [distance metric](
// http://www.compuphase.com/cmetric.htm) between two colors.
// Max distance is roughly 765 (3*255), for black & white.
// For our purposes, omitting the sqrt will not effect our results
rgbDistance(r, g, b) {
const [r1, g1, b1] = this
const rMean = Math.round((r1 + r) / 2)
const [dr, dg, db] = [r1 - r, g1 - g, b1 - b]
const [dr2, dg2, db2] = [dr * dr, dg * dg, db * db]
const distanceSq =
(((512 + rMean) * dr2) >> 8) +
4 * dg2 +
(((767 - rMean) * db2) >> 8)
return distanceSq // Math.sqrt(distanceSq)
},
}
export default Color