Skip to content

Commit

Permalink
add color
Browse files Browse the repository at this point in the history
  • Loading branch information
soerenmeier committed Jan 18, 2024
1 parent 9fc5f5e commit 3f3c243
Show file tree
Hide file tree
Showing 5 changed files with 399 additions and 7 deletions.
48 changes: 41 additions & 7 deletions src/animation/property.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Value from '../values/value.js';
import ColorValue from '../values/colorvalue.js';
import StyleValue from '../values/stylevalue.js';

export default class Property {
Expand Down Expand Up @@ -181,7 +182,9 @@ const STYLE_PROPS = {
'bottom': 'px',
'opacity': [1, null],
'padding': 'px',
'margin': 'px'
'margin': 'px',
'color': '#000',
'backgroundColor': '#fff'
}

export class StyleProp extends Property {
Expand All @@ -196,11 +199,11 @@ export class StyleProp extends Property {
this._defaultValue = new StyleValue([new Value(0, def)]);
this.unit = def;
} else {
this._defaultValue = new StyleValue(def);
this._defaultValue = StyleValue.parse(def);
this.unit = null;
}

// text|values
// text|color|values
this.kind = null;
}

Expand Down Expand Up @@ -238,6 +241,17 @@ export class StyleProp extends Property {
if (!to)
to = this.getValue(target);

// if one is a color both need to be a color
if (from.kind === 'color' || to.kind === 'color') {
if (from.kind !== to.kind)
throw new Error('expected two colors');

this.kind = 'color';
this.from = from;
this.to = to;
return;
}

// check that both are values
if (from.kind === 'text')
from = this.defaultValue();
Expand Down Expand Up @@ -293,10 +307,15 @@ export class StyleProp extends Property {
}

interpolate(pos) {
if (this.kind === 'text') {
return this._interpolateText(pos);
} else {
return this._interpolateValues(pos);
switch (this.kind) {
case 'text':
return this._interpolateText(pos);

case 'color':
return this._interpolateColor(pos);

default:
return this._interpolateValues(pos);
}
}

Expand Down Expand Up @@ -343,6 +362,21 @@ export class StyleProp extends Property {
throw new Error('expected from or to');
}

_interpolateColor(pos) {
const vals = [];

for (let i = 0; i < 4; i++) {
const f = this.from.values.values[i];
const t = this.to.values.values[i];

const dif = t - f;

vals.push(f + pos * dif);
}

return new StyleValue(new ColorValue(vals));
}

_interpolateValues(pos) {
// both values should be the same length

Expand Down
26 changes: 26 additions & 0 deletions src/tests/values.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,4 +106,30 @@ describe('values', () => {
expect(div.style.position).toBe(undefined);
expect(div.style.visibility).toBe(undefined);
});

it('colors', () => {
const ticker = new TestTicker;
const div = el();
div.computedStyle.color = 'rgb(120, 130, 20)';

const tl = timeline()
.add(div, {
color: 'black',
backgroundColor: 'hsl(10, 80%, 40%)',
duration: 10
})
.play();

ticker.run(0);
expect(div.style.color).toBe('rgba(120, 130, 20, 1.000)');
expect(div.style.backgroundColor).toBe('rgba(255, 255, 255, 1.000)');

ticker.run(5);
expect(div.style.color).toBe('rgba(60, 65, 10, 1.000)');
expect(div.style.backgroundColor).toBe('rgba(220, 152, 138, 1.000)');

ticker.run();
expect(div.style.color).toBe('rgba(0, 0, 0, 1.000)');
expect(div.style.backgroundColor).toBe('rgba(184, 48, 20, 1.000)');
});
});
167 changes: 167 additions & 0 deletions src/values/colorvalue.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import { COLORS } from './defaultcolors.js';

export default class ColorValue {
// [r, g, b, a?]
constructor(v) {
if (v.length === 3)
v = [...v, 1];

// always [r, g, b, a]
this.values = v;
}

static parse(v) {
if (Array.isArray(v)) {
if (v.length < 3 || v.length > 4)
throw new Error('expected array [r, g, b, a]');

return new ColorValue(v);
}

if (typeof v !== 'string')
throw new Error('expected a string or an array');

v = v.trim();

if (v.startsWith('#'))
return new ColorValue(parseHex(v.substring(1)));
if (v.startsWith('rgb'))
return new ColorValue(parseRgb(v));
if (v.startsWith('hsl'))
return new ColorValue(parseHsl(v));

if (v in COLORS)
return new ColorValue(COLORS[v]);

throw new Error('unknown color value ' + v);
}

// v needs to be a string
static mightBeAColor(v) {
v = v.trim();

return v.startsWith('#')
|| v.startsWith('rgb')
|| v.startsWith('hsl')
|| v in COLORS;
}

clone() {
return new ColorValue(this.values.slice());
}

toString() {
const [r, g, b, a] = this.values;
return `rgba(${[r, g, b].map(v => Math.round(v)).join(', ')}, ${
a.toFixed(3)
})`;
}
}

// hex without the hash
function parseHex(hex) {
if (hex.length !== 3 && hex.length !== 6 && hex.length !== 8)
throw new Error('unknown hex value ' + hex);

// Convert 3-character hex to 6-character hex
if (hex.length === 3)
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];

// Parse the hex values
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
let a = 255;

if (hex.length === 8)
a = parseInt(hex.substring(6, 8), 16);

return [r, g, b, a / 255];
}

// rgb needs to start with rgb
function parseRgb(rgb) {
const rgba = rgb.startsWith('rgba');
// remove the function name
rgb = rgb.substring(3 + rgba);
if (!rgb.startsWith('(') && !rgb.endsWith(')'))
throw new Error('invalid rgb function ' + rgb);

rgb = rgb.substring(1, rgb.length - 1);
const vals = rgb.split(',');

if ((rgba && vals.length !== 4) || vals.length !== 3)
throw new Error('invalid rgb function ' + rgb);

const r = parseInt(vals[0].trim());
const g = parseInt(vals[1].trim());
const b = parseInt(vals[2].trim());
const a = rgba ? parseFloat(vals[3].trim()) : 1;

return [r, g, b, a];
}

// hsl needs to start with hsl
function parseHsl(hsl) {
const hsla = hsl.startsWith('hsla');
// Remove the function name
hsl = hsl.substring(3 + hsla);
if (!hsl.startsWith('(') || !hsl.endsWith(')'))
throw new Error('invalid hsl function ' + hsl);

hsl = hsl.substring(1, hsl.length - 1);
let vals = hsl.split(',');

if ((hsla && vals.length !== 4) || (!hsla && vals.length !== 3))
throw new Error('invalid hsl function ' + hsl);

vals = vals.map(v => v.trim());

if (!vals[1].endsWith('%') && !vals[2].endsWith('%'))
throw new Error('invalid hsl function ' + hsl);

const h = parseInt(vals[0]);
const s = parseInt(vals[1].substring(0, vals[1].length - 1));
const l = parseInt(vals[2].substring(0, vals[2].length - 1));
const a = hsla ? parseFloat(vals[3]) : 1;

return hslToRgba([h, s, l, a]);
}

/// expects [h, s, l, a]
function hslToRgba(hsla) {
let [h, s, l, a] = hsla;

// normalize
h /= 360;
s /= 100;
l /= 100;

let r, g, b;
if (s === 0) {
r = g = b = l;
} else {
const q = l < .5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = Math.round(hueToRgb(p, q, h + 1 / 3) * 255, 0);
g = Math.round(hueToRgb(p, q, h) * 255, 0);
b = Math.round(hueToRgb(p, q, h - 1 / 3) * 255, 0);
}

return [r, g, b, a];
}

function hueToRgb(p, q, t) {
if (t < 0)
t += 1;
if (t > 1)
t -= 1;

if (t < 1 / 6)
return p + (q - p) * 6 * t;
if (t < 1 / 2)
return q;
if (t < 2 / 3)
return p + (q - p) * (2 / 3 - t) * 6;
return p;
}
Loading

0 comments on commit 3f3c243

Please sign in to comment.