Tuesday, 10 August 2021

js update keyframe property VANISHED

This is the strangest bug that I have ever met. blowed my mind.

function render_element(styles, el) {
  for (const [kk, vv] of Object.entries(styles)) {
el.style[kk] = vv;
  }
}

function capitalize(s){
return s[0].toUpperCase() + s.slice(1);
}

function swap(node1, node2) {
const afterNode2 = node2.nextElementSibling;
const parent = node2.parentNode;
node1.replaceWith(node2);
parent.insertBefore(node1, afterNode2);
}

function sortOnKeys(dict) {
var sorted = [];
for(var key in dict) {
    sorted[sorted.length] = key;
}
sorted.sort();
var tempDict = {};
for(var i = 0; i < sorted.length; i++) {
    tempDict[sorted[i]] = dict[sorted[i]];
}
return tempDict;
}

const delay = ms => new Promise(res => setTimeout(res, ms));

// round to 2 decimal places
const rit = val => Math.round(val * 100) / 100;

const unique = function(prefix){
if(!unique.cache)unique.cache = {};
if(unique.cache[prefix] == null)unique.cache[prefix] = 0;
unique.cache[prefix]++;
return prefix + unique.cache[prefix].toString();
}

// [1, 3], 3 -> [1, 2, 3]
const linespace = (tar, den) => Array.from({
  length: den
}, (x, i) => tar[0] + i * (tar[1] - tar[0]) / (den - 1));

const isInt = value => !isNaN(value) && (function(x) {
  return (x | 0) === x;
})(parseFloat(value));

// degree to radians
function d2r(deg) {
  return deg * Math.PI / 180;
}

// radians to degree
function r2d(rad) {
  return rad * 180 / Math.PI;
}

// theta = atan m
// convert y = kx + b to y = r x
// archive cartesian to polar and force domain to [0, 2pi] by
// regarding the line start from center and goes to one of the four quadrants
function get_rad(x, y) {
  var rad = Math.atan(y / x);
  if (x < 0 && y > 0) {
rad = Math.PI + rad;
  } else if (x < 0 && y < 0) {
rad = Math.PI + rad;
  } else if (x > 0 && y < 0) {
rad = 2 * Math.PI + rad;
  }
  return rad;
}

// pass in one point and a radians to ratate
// return a rotated dot
function rotate_by_origin(pt, rt) {
  var phi = get_rad(pt.x, pt.y) + rt;
  var r = (pt.x ** 2 + pt.y ** 2) ** 0.5;
  if (phi > 2 * Math.PI) {
phi -= 2 * Math.PI;
  }
  if (0 < phi < Math.PI * 0.5) {
return {
  x: Math.cos(phi) * r,
  y: Math.sin(phi) * r
};
  } else if (Math.PI * 0.5 < phi < Math.PI) {
phi -= 0.5 * Math.PI;
return {
  x: -Math.cos(phi) * r,
  y: Math.sin(phi) * r
};
  } else if (Math.PI < phi < Math.PI * (2 / 3)) {
phi -= Math.PI;
return {
  x: -Math.cos(phi) * r,
  y: -Math.sin(phi) * r
};
  } else {
phi -= (2 / 3) * Math.PI;
return {
  x: Math.cos(phi) * r,
  y: -Math.sin(phi) * r
};
  }
}

// pass in two dots and one radians
// return a dot
// rotate pt by ori with rt radians
function rotate_one(ori, pt, rt) {
  var bk = {
x: pt.x - ori.x,
y: pt.y - ori.y
  };
  var rbk = rotate_by_origin(bk, rt);
  return {
x: rbk.x + ori.x,
y: rbk.y + ori.y
  };
}

// pass in side, center, mouse
function nPolyDots(n, cen, pt) {
  var inde = 2 * Math.PI / n;
  var cont = [];
  cont.push(pt);
  for (let i = 1; i < n; i++) {
cont.push(rotate_one(cen, pt, i * inde));
  }
  return cont;
}

class XY{
constructor(s, x, y){
    this.s = s;
    this.x = x;
    this.y = y;

    this.concat = this.concat.bind(this);
    this.calcX = this.calcX.bind(this);
    this.calcY = this.calcY.bind(this);
    this.parse = this.parse.bind(this);
}
calcX(func){this.x = func(this.x);return this}
calcY(func){this.y = func(this.y);return this}
concat(){return new XY(this.s, this.x, this.y)}
parse(){return this.s + rit(this.x).toString() + ' ' + rit(this.y).toString()}
}

// let xy = new XY('L', 10, 12);
// console.log(xy.addX(10).addY(-10).mulX(2));

class ML{
constructor(d){
    if(d){
        // sep always parsed to space
        let sep = d.includes(',') ? ',' : ' ';
        this.d = d;
        this.zflag = d.includes('Z');
        this.xys = d.split('L').map(st=>isInt(st[0]) ? new XY("L", ...st.split(sep).map(dd=>rit(parseFloat(dd)))) : new XY('M', ...st.split(sep).map((dd, ii)=>ii === 0 ? rit(parseFloat(dd.substring(1))) : rit(parseFloat(dd)))));
        this.calcX = this.calcX.bind(this);
        this.calcY = this.calcY.bind(this);
        this.concat = this.concat.bind(this);
        this.parse = this.parse.bind(this);
    }
}

calcX(func){this.xys.forEach(xy=>xy.calcX(func));return this}
calcY(func){this.xys.forEach(xy=>xy.calcY(func));return this}

concat(){
    let ml = new ML();
    ml.xys = this.xys.map(xy=>xy.concat());
    ml.zflag = this.zflag;
    return ml;
}

get length(){return this.xys.length;}

parse(){
    return this.xys.map(xy=>xy.parse()).join(' ') + (this.zflag ? 'Z' : '');
}
}

class Vector{
constructor(
    parent_el,
    id,
    width,
    height,
){
    this.parent_el = parent_el;
    this.id = id;
    this.width = width;
    this.height = height;

    this.widgets = [];
    this.parent_el.innerHTML += `<svg viewBox="0 0 ${width} ${height}" xmlns="http://www.w3.org/2000/svg"></svg>`
    this.el = this.parent_el.lastChild;
    this.viewBox = this.viewBox.bind(this);
    this.addPath = this.addPath.bind(this);
}

viewBox(dx, dy){
    this.width += dx; 
    this.height += dy; 
    this.el.setAttribute('viewBox', `0 0 ${this.width} ${this.height}`)
    return this;
}

addPath(widget){
    this.widgets.push(widget);
    widget.init(this);
    return widget;
}
}

class Path{
constructor(
    id, 
    str,
    width,
    height,
){
    this.id = id;
    this.str = str;
    this.width = width;
    this.height = height;
    this.init = this.init.bind(this);
    this.fit = this.fit.bind(this);
    this.frontMost = this.frontMost.bind(this);
    this.front = this.front.bind(this);
    this.backMost = this.backMost.bind(this);
    this.back = this.back.bind(this);

    this.animsvg = [];
    this.animcss = [];
}

get el(){return this.host.el.getElementById(this.id)}

init(vector){
    this.host = vector; // Vector instance (SVG)
    this.host.el.innerHTML += this.str;
}

fit(){
    this.host.viewBox(-this.host.width + this.width, -this.host.height + this.height);
}

async frontMost(ms){
    if(ms){await delay(ms)}
    let path_el = this.el;
    this.host.el.removeChild(path_el);
    this.host.el.appendChild(path_el);
    return this;
}

async front(ms){
    if(ms){await delay(ms)}
    let kids = [];
    for(let ii = 0; ii < this.host.el.children.length; ii++){
        kids.push(this.host.el.children[ii]);
    }
    let path_el = this.el;
    let ind = kids.indexOf(path_el);
    this.host.el.removeChild(path_el);
    this.host.el.insertBefore(path_el, kids[ind + 2]);
    return this;
}

async back(ms){
    if(ms){await delay(ms)}
    let kids = [];
    for(let ii = 0; ii < this.host.el.children.length; ii++){
        kids.push(this.host.el.children[ii]);
    }
    let path_el = this.el;
    let ind = kids.indexOf(path_el);
    this.host.el.removeChild(path_el);
    this.host.el.insertBefore(path_el, kids[ind - 1]);
    return this;
}

async backMost(ms){
    if(ms){await delay(ms)}
    let path_el = this.el;
    this.host.el.removeChild(path_el);
    this.host.el.insertBefore(path_el, this.host.el.children[0]);
    return this;
}

static star(
    id, 
    radius, 
    deg, // rotate degree
    padding, 
    fill, 
    stroke,
    strokeWidth,
){
    // star is composed by two 5 side polygons
    // wheares sin54 r = sin18 R
    let r_out = radius;
    let r_in = Math.sin(d2r(18)) / Math.sin(d2r(54)) * r_out;
    let center = {x: r_out, y: r_out};
    let s_out_pre_r = {x: r_out, y: 2 * r_out - padding};
    let s_out = rotate_one(center, s_out_pre_r, d2r(180 + deg));
    let s_in_pre_r = {x: r_out, y: r_out + r_in};
    let s_in = rotate_one(center, s_in_pre_r, d2r(216 + deg));
    let p_out = nPolyDots(5, center, s_out);
    let p_in = nPolyDots(5, center, s_in);
    let c_out = 0;
    let c_in = 0;
    let pts = [];
    for(let ii = 0; ii < 10; ii++){
        if(ii % 2 === 0){
            pts.push({x: rit(p_out[c_out].x), y: rit(p_out[c_out].y)});
            c_out++;
        }else{
            pts.push({x: rit(p_in[c_in].x), y: rit(p_in[c_in].y)});
            c_in++;
        }
    }
    let path = Array.from(pts, (ii, cc)=>(cc === 0 ? "M" : "L") + ii.x.toString() + ',' + ii.y.toString()).join(' ') + 'Z';
    let ra = radius * 2;
    return new Path(
        id, 
        `<path id=${id} d="${path}" fill="${fill}" stroke="${stroke}" stroke-linejoin="round" stroke-linecap="round" stroke-width="${strokeWidth}"></path>`,
        ra,
        ra,
    );
}

addAnimStatic(anim){
    this.animcss.push(anim);
    anim.init(this);
    return anim;
}
}

class Keyframe{
constructor(id){
    this.id = id;
    this.cache = [];
    this.tag = document.createElement('style');
    // bind
    this.add = this.add.bind(this);
    this.remove = this.remove.bind(this);
    this.replace = this.replace.bind(this);
}

add(di){
    this.cache.push(di);
    document.head.appendChild(this.tag);
    return ()=>this.cache.indexOf(di);      
}

pop(ind){
    if(ind == null)return null;
    this.cache.splice(ind, 1);
    this.push();
    return this.cache.length;
}

replace(ind, di){
    this.cache[ind] = di;
}

push(){
    let di = {};
    this.cache.forEach(keyframe=>{
        for(const [time, css] of Object.entries(keyframe)){
            if(!di[time]){
                di[time] = Object.assign({}, css);
            }else{
                for(const [prop, val] of Object.entries(css)){
                    if(Object.keys(di[time]).includes(prop)){
                        if(prop === 'transform'){
                            di[time][prop] += ' ' + css[prop];
                        }else{
                            di[time][prop] = css[prop];
                        }
                    }else{
                        di[time][prop] = css[prop];
                    }
                }
            }
        }   
    });     
    // parse a string
    let st = '@keyframes ' + this.id + ' {';
    for(const [time, css] of Object.entries(di)){
        st += time.toString() + '%{';
        for(const [prop, val] of Object.entries(css)){
            st += prop + ': ' + val + ';';
        }
        st += '}';
    }
    st += '}';
    this.tag.innerHTML = st;
}

remove(){
    document.head.removeChild(this.tag);
}
}

// use a panel to integrite all
class AnimStatic{
constructor(
    name, // func name
    func, // static animation function
    di, // param to func
    tflag, // bool transform flag
    keyframe,  
    styles,
){
    this.name = name;
    this.func = func;
    this.di = di;
    this.tflag = tflag; 
    this.keyframe = keyframe;
    this.styles = styles;
    this.init = this.init.bind(this);
    this.remove = this.remove.bind(this);
    this.update = this.update.bind(this);
}

init(path){
    this.host = path;
    if(!this.host.keyframe)this.host.keyframe = new Keyframe(path.id + 'Anim');
    this.ind = this.host.keyframe.add(this.keyframe);
    this.host.keyframe.push();
    render_element(this.styles, path.el);
    if(path.el.style.animation !== ''){
        path.el.style.animation += `, ${this.host.keyframe.id} ${this.di.dur}ms ${this.di.begin}ms ${this.di.timing} ${this.di.count}`;
    }else{
        path.el.style.animation = `${this.host.keyframe.id} ${this.di.dur}ms ${this.di.begin}ms ${this.di.timing} ${this.di.count}`;
    }
}

remove(){
    if(!this.host.keyframe.pop(this.ind())){
        let disp = window.getComputedStyle(this.host.el, null).display;
        this.host.el.style.animation = 'none';
        setTimeout(()=>{
            this.host.el.style.animation = disp;
        }, 0);
    }else{
        let anim = this.host.el.style.animation;
        let disp = window.getComputedStyle(this.host.el, null).display;
        this.host.el.style.animation = 'none';
        this.host.el.style.display = 'none';
        setTimeout(()=>{
            this.host.el.style.animation = anim;
            this.host.el.style.display = disp;
        }, 0);
    }
}

update(di){
    console.log(['update', this.host]);
    Object.assign(this.di, di);
    // parse a new keyframe
    Object.assign(this, this.func(this.di));
    this.host.keyframe.replace(this.ind(), this.keyframe);
    this.host.keyframe.push();
    let anim = this.host.el.style.animation;
    this.host.el.style.animation = 'none';
    setTimeout(()=>{
        this.host.el.style.animation = anim;
    }, 0);
    console.log(['aftupdate', this.host]);
}

static wrapper(prop, values, percents){
    let di = {};
    percents.forEach((pc, ii)=>{
        di[pc] = {[prop]: values[ii]};
    });
    return di;
}

/*
interface{
    begin: Number,
    dur: Number, 
    values: any,
    percents: Array, 
    timing: String, default to 'ease-in-out', 
    count: String, default to 'indefinite',
    styles: Object, default to {}
}
*/

static opacity(di){
    return new AnimStatic(
        'opacity', 
        AnimStatic.opacity, 
        di, 
        false, 
        AnimStatic.wrapper('opacity', di.values, di.percents), 
        di.styles,
    )
}

// transform frame
static translate(di){
    return new AnimStatic(
        'translate', 
        AnimStatic.translate, 
        di, 
        true, 
        AnimStatic.wrapper('transform', di.values.map(val=>`translate(${val[0]}, ${val[1]})`), di.percents),
        di.styles,
    )
}

static scale(di){
    return new AnimStatic(
        'scale', 
        AnimStatic.scale, 
        di,  
        true, 
        AnimStatic.wrapper('transform', di.values.map(val=>`scale(${val[0]}, ${val[1]})`), di.percents),
        di.styles,
    )
}

static rotate(di){
    return new AnimStatic(
        'rotate', 
        AnimStatic.rotate,
        di, 
        true, 
        AnimStatic.wrapper('transform', di.values.map(val=>`rotate(${val}deg)`), di.percents),
        di.styles,
    )
}

static skew(di){
    return new AnimStatic(
        'skew', 
        AnimStatic.skew, 
        di,  
        true, 
        AnimStatic.wrapper('transform', di.values.map(val=>`skew(${val}deg)`), di.percents),
        di.styles,
    )
}
}

let svg = new Vector(
document.body,
'light',
200,
200,
);
svg.viewBox(400, 400);

let star = svg.addPath(
Path.star(
    'star',
    150,
    0,
    10,
    'gray',
    'lightgreen',
    15,
)
);

let a1 = star.addAnimStatic(
AnimStatic.translate({
    begin: 100, 
    dur: 1000, 
    values: [['0', '0'], ['200px', '200px'], ['200px', '0']], 
    percents: [0, 50, 100],
    timing: 'ease-in-out',
    count: 'infinite',
    styles: {},
})
);

setTimeout(()=>{
a1.update({
    values: [['0', '0'], ['200px', '200px'], ['0', '200px']], 
});
setTimeout(()=>{
    console.log(['sec', a1.host]);
    a1.update({
        values: [['0', '0'], ['200px', '0'], ['200px', '200px']], 
    });
    console.log(['aftsec', a1.host]);
}, 900);
}, 900);
svg{
width: 200px;
height: 200px;
}

body{
position: fixed;
margin: 0;
padding: 0;
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}

I am trying to apply css keyframe animation to elements by javascript.

  1. addAnimStatic will create a <style> tag into <header> and the name of the animation will be added to the widget.
  2. update will overwrite the keyframe.

here is the part that went wrong without any logic. please take a look at the end of the script and pay close attention to the console, I am trying to update the animation twice, the first one works, but the second.

before the beginning of second animation, I logged a1.host which the result is shown. however, in the next line, when it got to update function, the host property magically disappeared.

which there is absolutely no conduction in between. When the scope jumped from setTimeout to AnimateStatic.update, the property host just vanished.

I will be so glad if you can give me a hand.


after one day of trying, I still have no idea why the property just vanished.


Minimum working example

codepen


In the original design I was meant to use click to trigger the animation update, it does not work as well, the same error undefined variable this.host reported.



from js update keyframe property VANISHED

No comments:

Post a Comment