Skip to content

Commit

Permalink
add class management
Browse files Browse the repository at this point in the history
  • Loading branch information
soerenmeier committed Jan 14, 2024
1 parent 1a1e8fd commit 07fd1fe
Show file tree
Hide file tree
Showing 7 changed files with 280 additions and 25 deletions.
32 changes: 16 additions & 16 deletions src/animation/animation.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,6 @@ class PropertyAnimation {
this.iniFrom = null;
this.iniTo = null;

this.from = null;
this.to = null;

if (typeof value === 'object') {
if ('from' in value)
this.iniFrom = maybeReactiveValue(this.prop, value.from);
Expand All @@ -123,6 +120,12 @@ class PropertyAnimation {
if (!this.iniFrom && !this.iniTo)
throw new Error('from or to expected');
} else if (typeof value === 'function') {
if (!this.prop.allowsValueFn()) {
throw new Error(
'prop ' + prop + ' does not allow value functions'
);
}

this.valueFn = value;
} else {
this.iniTo = this.prop.parseValue(value);
Expand All @@ -131,27 +134,26 @@ class PropertyAnimation {

// calculate from and to position
init(target) {
// there is nothing to initialize since on every render we call the
// value Fn
if (this.valueFn)
return;

// init from
let from = null;
if (typeof this.iniFrom === 'function')
this.from = this.prop.parseValue(this.iniFrom(target.target));
from = this.prop.parseValue(this.iniFrom(target.target));
else
this.from = this.iniFrom;
if (!this.from)
this.from = this.prop.getValue(target);
from = this.iniFrom;

// init to
let to = null;
if (typeof this.iniTo === 'function')
this.to = this.prop.parseValue(this.iniTo(target.target));
to = this.prop.parseValue(this.iniTo(target.target));
else
this.to = this.iniTo;
if (!this.to)
this.to = this.prop.getValue(target);
to = this.iniTo;

if (this.to.unit !== this.from.unit)
throw new Error(this.from.unit + ' != ' + this.to.unit);
this.prop.init(target, from, to);
}

restoreBefore(target) {
Expand All @@ -165,9 +167,7 @@ class PropertyAnimation {
return;
}

const dif = this.to.num - this.from.num;

this.prop.setValue(target, this.from.cloneAdd(pos * dif));
this.prop.setValue(target, this.prop.interpolate(pos));
}
}

Expand Down
168 changes: 168 additions & 0 deletions src/animation/property.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,37 @@ export default class Property {
constructor(name) {
// this is the property name
this.name = name;

this.from = null;
this.to = null;
}

allowsValueFn() {
return true;
}

/**
* From or to might be null
*/
init(target, from, to) {
if (!from)
from = this.getValue(target);

if (!to)
to = this.getValue(target);

if (!from.unit || from.unit !== to.unit)
throw new Error(from.unit + ' != ' + to.unit);

this.from = from;
this.to = to;
}

/**
* Gets called with the value of a property
*
* For examply during initializiation
*/
parseValue(val) {
return Value.parse(val);
}
Expand All @@ -23,6 +52,18 @@ export default class Property {
return val;
}

/**
* Pos needs to be between 0 and 1
*
* From and to will be values returned from getvalue or defaultValue
* and validated by validateFromTo
*/
interpolate(pos) {
const dif = this.to.num - this.from.num;

return this.from.cloneAdd(pos * dif);
}

setValue(target, val) {
return target.setValue(this.name, val);
}
Expand Down Expand Up @@ -180,6 +221,131 @@ export class StyleProp extends Property {
}
}

/**
* In most cases we wan't to add the class at pos 0
* and in reverse remove it at pos 0
*
* so this behaviour should be in str|{ to }
*
* If we wan't to remove the class at pos 1
* and in reverse add it at pos 1
* we do it in { from }
*/
export class ClassNameProp extends Property {
constructor(name) {
super(name);
}

allowsValueFn() {
return false;
}

init(target, from, to) {
this.from = from ?? [];
this.to = to ?? [];

this.preStates = new Map;
this.states = new Map;

for (const cls of [...this.from, ...this.to]) {
const exists = target.hasClass(cls);
this.preStates.set(cls, exists);
this.states.set(cls, exists);
}
}

parseValue(val) {
if (Array.isArray(val)) {
return val.map(v => {
if (typeof v !== 'string')
throw new Error('cls only accepts strings[]');

return v;
});
}

if (typeof val === 'string') {
return [val];
}

throw new Error('cls only accepts strings and strings[]');
}

defaultValue() {
throw new Error('unused');
}

getValue(_target) {
throw new Error('unused');
}

interpolate(pos) {
/**
* ## to
* add the class at pos >0
* and in reverse remove it at pos 0
*
* ## from
* remove the class at pos <1
* and in reverse add it at pos 1
*/

let obj = {
add: [],
remove: []
};

if (pos <= 0) {
obj.remove = this.to;
} else {
obj.add = this.to;
}

if (pos >= 1) {
obj.remove = this.from;
} else {
obj.add = [...obj.add, ...this.from];
}

return obj;
}

setValue(target, val) {
let { add, remove } = val;
add = add ?? [];
remove = remove ?? [];

if (!Array.isArray(add) || !Array.isArray(remove))
throw new Error('add or remove expected');

add.forEach(cls => {
const exists = this.states.get(cls);
if (!exists) {
target.addClass(cls);
this.states.set(cls, true);
}
});

remove.forEach(cls => {
const exists = this.states.get(cls);
if (exists) {
target.removeClass(cls);
this.states.set(cls, false);
}
});
}

removeValue(target) {
this.preStates.forEach((exists, cls) => {
if (exists)
target.addClass(cls);
else
target.removeClass(cls);
});
this.states = new Map(this.preStates);
}
}

export function newProperty(prop, targetType = '') {
if (targetType !== 'dom')
throw new Error('only dom is supported as target');
Expand All @@ -190,6 +356,8 @@ export function newProperty(prop, targetType = '') {
return new TransformXY(prop);
if (prop in STYLE_PROPS)
return new StyleProp(prop);
if (prop === 'cls')
return new ClassNameProp(prop);

throw new Error('unknown prop ' + prop);
}
33 changes: 32 additions & 1 deletion src/tests/timing.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,31 +91,62 @@ describe('timing', () => {
expect(timing.state).toBe(STATE_BEFORE);
expect(timing.position).toBe(0);

// <
timing.reverse();
// inverse
expect(timing.state).toBe(STATE_AFTER);
expect(timing.position).toBe(0);

// >
timing.reverse();
expect(timing.state).toBe(STATE_BEFORE);
expect(timing.position).toBe(0);

// >
timing.advance(25);
expect(timing.state).toBe(STATE_RUNNING);
expect(timing.position).toBe(0.25);

// <
timing.reverse();
expect(timing.state).toBe(STATE_RUNNING);
expect(timing.position).toBe(0.25);

// <
timing.advance(24);
expect(timing.state).toBe(STATE_RUNNING);
expect(timing.position.toFixed(3)).toBe('0.010');

// <
timing.advance(1);
expect(timing.state).toBe(STATE_ENDED);
expect(timing.position).toBe(0);

// >
timing.reverse();
expect(timing.state).toBe(STATE_START);
expect(timing.position).toBe(0);

// ><
timing.seek(0.75);
timing.reverse();
expect(timing.position).toBe(0.75);

// <
timing.advance(10);
expect(timing.position).toBe(0.65);


const timing2 = new Timing({
duration: 100
});

timing2.advance(100);
expect(timing2.state).toBe(STATE_ENDED);
expect(timing2.position).toBe(1);

timing2.reverse();
expect(timing2.state).toBe(STATE_START);
expect(timing2.position).toBe(1);
});

it('repeat', () => {
Expand Down
23 changes: 18 additions & 5 deletions src/tests/usecases.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,8 @@ describe('usecases', () => {
ctn.scrollHeight = '100px';

const tl = timeline()
.add(itm, {
// clsAdd: 'open',
duration: 100
.set(itm, {
cls: 'open'
})
.add(ctn, {
maxHeight: reactive(el => el.scrollHeight),
Expand All @@ -25,15 +24,29 @@ describe('usecases', () => {

// // on click
tl.play();
ticker.run(1);
expect(ctn.style.maxHeight).toBe('1.000px');
ticker.run(5);
expect(ctn.style.maxHeight).toBe('5.000px');
expect(itm.classList.contains('open')).toBe(true);

ticker.run();
expect(ctn.style.maxHeight).toBe('100.000px');
expect(itm.classList.contains('open')).toBe(true);

// now a resize comes
ctn.scrollHeight = '200px';
tl.update();
expect(ctn.style.maxHeight).toBe('200.000px');
expect(itm.classList.contains('open')).toBe(true);

// on click
tl.reverse();
tl.play();
ticker.run(12);
expect(ctn.style.maxHeight).toBe('176.000px');
expect(itm.classList.contains('open')).toBe(true);

ticker.run();
expect(ctn.style.maxHeight).toBe(undefined);
expect(itm.classList.contains('open')).toBe(false);
});
});
Loading

0 comments on commit 07fd1fe

Please sign in to comment.