Cashflow Profiles and Reserves: Actuarial Terminology I
visualization
actuarial
Actuarial terminology, visualized
Author
Published
July 23, 2024
This post is especially intended for interested non-actuaries, and it’s simplified.
Feel free to send me feedback, suggestions, and to share!
Life insurance
In this post I aim to briefly illustrate some basic actuarial terminology for life insurance.
Some usual important features of life insurance business are:
- Risk
- Long durations and, often:
- Recurring payments
Many actuarial things are relatable to other industries - especially other industries involving recurring payments (like SaaS or subscription businesses), and those that involve risk.
Cashflow Profiles
The simplest life insurance contracts include two main cashflows that I’ll focus on here; from the insurance company perspective:
- Premiums are the main income and
- Claims: paid out on death of the life insured, are the main outgo
I’ll be considering a 20-year term insurance contract, which pays out a fixed amount if the life insured dies during the 20-year term, and otherwise pays out nothing (no surrender value).
Claims
For this type of contract, we expect claims to be higher later: when policy holders are older and therefore have a higher risk of mortality.
Code
claims = [0,..._.range(0,19.1).map(year => 1.35**year)]
w = Math.min(300,width-50)
viz_claims = embed('#claims-el', ({
width: w,
height: 100,
border: null,
//background: 'pink',
title: {text:'expected claims by year', fontSize:15, font:'monospace'},
titleFontSize: 40,
data: {values: claims.map((d,i) => ({year:i, value:d}))},
mark: {type:'bar', tooltip:true},
encoding: {
x: {field: 'year', type: 'ordinal', title: 'year', scale: {domain:_.range(0,21.1)},axis: {grid: false, values:[1,10,20], labelAngle: 0}},
y: {field: 'value', type: 'quantitative', format:',.2f', title:'amount', axis: {grid: false}},
color: {value: 'darkorange'}
},
"config": {
"style": {
"cell": {
"stroke": "transparent"
}
}
}
}))
Premiums
We expect premiums on the other hand stay relatively flat:
Code
premiums = [0,..._.range(0,19.1).map(year => 100-year*1.5)]
viz_premiums = embed('#premiums-el', ({
width: w,
height: 100,
//background: 'lightgreen',
title: {text:'expected premiums by year', fontSize:15, font:'monospace'},
data: {values: premiums.map((d,i) => ({year:i, value:d}))},
mark: {type:'bar', tooltip:true},
encoding: {
x: {field: 'year', type: 'ordinal', title: 'year', scale: {domain:_.range(0,21.1)}, axis: {grid: false, values:[1,10,20], labelAngle: 0}},
y: {field: 'value', type: 'quantitative', format:',.2f', title:'amount', scale: {domain:[0,300]}, axis: {grid: false}},
color: {value: 'steelblue'}
},
"config": {
"style": {
"cell": {
"stroke": "transparent"
}
}}
}))
Decrements 📉
This premium pattern is reducing: not flat. It’s reducing because of ‘decrements’ reducing the number of customers expected to continue paying premiums.
Decrements are mainly due to cancellations here but also due to deaths.
In insurance we tend to refer to cancellations as ‘lapses’; in other industries the same thing is known as ‘churn’.
Lapse or churn risk is often a key risk for life insurance companies, as is mortality risk in a product such as this.
More about decrements: modelling and monitoring them, another time. Actuaries look at these very closely!
Premiums - Claims
Combining premiums and claims gives us the following:
This is a stacked bar chart where bars represent cashflows: premiums are positive, and claims are negative.
Circle symbols represent their sum, which indicates a profit profile for the contract.
- symbols indicate profitable years: where premiums are higher than claims
- symbols indicate loss-making years: where premiums are too low to cover claim payments
Notice that there are large expected losses in the later stages of the contract: occuring after some sustained profitability
This is a very common cashflow profile in life insurance contracts. And points to some reasons why trust is important when it comes to buying life insurance.
Reserves
It’s bad to expect to lose: especially if doing so means you can’t make payments you can be held accountable to - which puts you out of business.
Because of this (and because of regulation), insurance companies create reserves: they set aside money so they can pay for or cover future losses.
Code
mutable e = false
viewof button = {
// this div should have been a button - practical issues?
// and surely pressed effect another way to get my 'stateful button' effect (better than checkbox?)
// e.g. https://www.w3schools.com/howto/tryit.asp?filename=tryhow_css_buttons_animate3
const el = html`<div id="reserve-effect-button" class="action active-button">Check to <span style="font-weight:bold">show the effect of a reserve</span> to cover future losses <input class="actionCheckbox" type="checkbox" /></div><!-- should be a label! -->`
el.value = {value: false, disabled: false}
el.onclick = () => {
if (el.value.disabled) return;
el.value.value = !el.value.value
if (el.value.value)
el.firstChild.classList.remove('active-button')
else
el.firstChild.classList.add('active-button');
el.getElementsByTagName('input')[0].checked = el.value.value
el.value.disabled = true;
set(viewof step, el.value.value ? 1 : 0)
el.dispatchEvent(new Event("input", {bubbles: true}));
}
return el
}
Code
Here we set aside money in profitable years, that we can ‘release’ in later years to cover losses.
The new profit profile is much less wild (it’s much more smooth) - but notice in particular, expected losses are covered ✅
symbols: indicating large expected loses, are no more. 🦺
Prudence
Insurance companies accept risk as a routine, so that merely preparing to pay expected claims - as above, is bound to be disastrous in the ordinary course of time.
They must also be prepared for higher than expected claims: amongst other risks.
So, their reserve calculations will often add some level of prudence.
Insurance companies will consider a lot of things in their calculation and presentation of reserves: especially regulatory rules.
Solvency II
In the EU, Solvency II regulation talks about a 1-in-200-year-event calibration; but that’s a different blog post!
Now you can add a new reserve calculated as some proportion of all expected future claims:
Code
Code
viewof slider = {
const el = html`<div class="action active-button slider-box" style="cursor:auto"><label style="" for="slider">add a <span style="font-weight:bold">% of expected future claims reserve</span>: drag the slider <span style="font-style:normal">↔️</span></label><input id="slider" type="range" value="0" min="0" max=".45" step=".005" style="cursor:pointer" /></div>`
el.value = 0;
el.oninput = e => {
el.value = e.srcElement.valueAsNumber
//e.val
}
return el
}
Code
md`This reserve is calculated as <span style="white-space: nowrap"><span title="Drag slider to adjust" style="font-weight:bold; border-bottom:1px dashed grey">${d3.format('.1%')(slider)}</span> ↔️</span> of all **expected future claims**.
<div style="${slider < 0.001 ? "text-decoration: line-through" : ""}"><p>On sale of the policy the insurance company faces an <span class="into-adv-reserve">immediate loss</span> to set aside money for this reserve: an initial strain [common in insurance](https://en.wikipedia.org/wiki/New_business_strain).</p>
<p>But, subsequently it expects <span class="out-adv-reserve">reserve releases</span> leading to profits - since each year expected *future claims* get smaller, until - at the end - future claims and consequently the reserve calculation are 0.</p></div>
`
Reserve calculations might be ‘notional’ and money set aside by the insurance company might get returned to the insurance company, but this still serves a purpose.
By adding prudence in reserve calculations, money is available to cover some worse-than-expected experience, until some associated risk passes. 🦺
In this post and others I’m using calculang: a language for calculations I develop that helps provide structure for calculations and numbers. To find out more about calculang, you can check calculang.dev.
ignore
New Business Strain
Above we saw that reserves can (and usually do) result in capital being needed at outset: that is, capital being needed just to be able to make a sale and continue meeting regulatory requirements.
Insurance companies will therefore measure this “New Business Strain” and monitor their sales and sales strategies according to available capital.
There are lots of ways to manage capital requirements: for example reinsurance: where the insurance company insures some part of it’s risk with reinsurance companies. Good planning about capital utilization (including it’s release) is also important.
The profit profile I outlined above goes a long way to explain patterns in my [linked visuals] blog post.
Code
embed_ = require("vega-embed")
viewof actions = Inputs.toggle({value:false})
embed = (a,b,options) => embed_(a,b, {renderer:'svg', /*theme:'carbong10',*/ actions, ...options});
//embed('#dist1',dist_spec, {renderer: 'canvas', actions:false})
set = (input, value) => {
input.value = value;
input.dispatchEvent(new Event("input", {bubbles: true}));
}
Code
vl5 = require('vega-lite@5')
vl4 = require('vega-lite@4')
height = 200
function area_spec_new_format(reserves, params, w) {
let color_scale;
if (reserves == 0)
color_scale = {
domain: ["premiums", "mclaims"],
range: ["steelblue", "darkorange"]
};
else if (reserves == 1)
color_scale = {
domain: ["premiums", "mclaims", "mΔreserve"],
range: ["steelblue", "darkorange", "yellow"]
};
else
color_scale = {
domain: ["premiums", "mclaims", "mΔreserve", "mΔadverse_reserve"],
range: ["steelblue", "darkorange", "yellow", "#f1c0e8"]
};
return {
$schema: "https://vega.github.io/schema/vega-lite/v5.json",
data: { name: "projection" },
datasets: { projection: [] },
title: {text:'expected cashflows', fontSize:20, font:'monospace'},
height,
//title: {text:'profit profile', fontSize:20, font:'monospace'},
width: w,
transform: [
{ filter: reserves==0 ? "(datum.formula != 'mΔreserve')&& datum.year_in != 0" : reserves == 1 ? 'datum.year_in != 0' : '1' },
{
calculate: "datum.partition == 'subtract' ? -datum.value : datum.value",
as: "value"
}
],
encoding: {
order: { field: "formula" },
x: {
field: "year_in",
title: 'year',
type: "ordinal",
labelAngle: 0,
axis: { grid: false, values: [1, 10, 20] },
scale: { nice: false, domain: _.range(0,21.1) }
},
y: {
field: "value",
title: null,
type: "quantitative",
format:',.2f',
scale: { domain: [-400, 450], nice: false },
axis: {
grid: false,
gridColor: "hotpink",
gridWidth: 5,
//labelAngle: -45,
values: [0 ,100,-200]
}
}
},
layer: [
{
transform: [
{ filter: "abs(datum.value)>0.01" },
{
filter:
"datum.formula != 'profit' && (datum.partition == 'keep' || datum.partition == 'add' || datum.partition == 'subtract')"
}
],
mark: { type: "bar", tooltip:true /*, width: 12*/ },
...(params && {
params: [
{
name: "cashflow", // DN: as long as not used in spec (=> data() function called in expressions), then gemini transitions work
select: { type: "point", fields: ["formula"] },
bind: "legend" // bound but not visible or used !! be aware of failing gemini/vl4 compile?
}
]
}),
encoding: {
opacity: {
//condition: { param: "cashflow", value: 1 },
condition: {
test: 'datum.partition == "add" || datum.partition == "subtract"',
value: 1
},
value: reserves == 0 ? 1 : 0.6
},
strokeWidth: {
value: 0,
condition: {
test: 'datum.partition == "add" || datum.partition == "subtract"',
value: 2
}
},
stroke: {
value: "darkgreen",
condition: { test: 'datum.partition == "subtract"', value: "red" }
},
color: {
legend: null,
field: "formula",
type: "nominal",
scale: color_scale
}
}
},
{
transform: [
{ filter: "datum.formula == 'profit' && datum.partition == 'B'" },
{ calculate: "50+width/8", as: "a" }
],
mark: {
type: "point",
baseline: "middle",
tooltip: true,
dy: 1,
size: 90,
filled: true
},
encoding: {
opacity: { value: 1 },
strokeWidth: { value: 2 },
stroke: { value: "black" },
color: {
field: "value",
type: "quantitative",
scale: {
type: "threshold",
domain: [-0.1, 0.1],
range: ["red", "lightblue", "green"]
},
legend: null
}
}
},
{
// 'ghost' marks for visual comparison
transform: [
{ filter: "datum.formula == 'profit' && datum.partition == 'A'" }
],
mark: {
type: "point",
baseline: "middle",
tooltip: true,
dy: 1,
size: 90,
filled: false
},
encoding: {
opacity: { value: 0.15 },
strokeWidth: { value: 2 },
stroke: { value: "black" }
/*color: {
field: "value",
type: "quantitative",
scale: {
type: "threshold",
domain: [-0.1, 0.1],
range: ["red", "lightblue", "green"]
},
legend: null
}*/
}
}
],
/*"config": {
"style": {
"cell": {
"stroke": "transparent"
}
}}*/
};
}
//vlspec = area_spec_new_format(i, params, w)
vgspec_profile = function ({reserves, vl}) {
// using https://github.com/uwdata/gemini/blob/master/src/util/vl2vg4gemini.js
function appendNamesOnGuides(vgSpec) {
if (vgSpec.axes) {
vgSpec.axes.forEach((axis) => {
if (!axis.encode) {
axis.encode = { axis: { name: axis.scale } };
} else {
axis.encode.axis = { ...axis.encode.axis, name: axis.scale };
}
});
}
if (vgSpec.legends) {
vgSpec.legends.forEach((legend, i) => {
if (!legend.encode) {
legend.encode = { legend: { name: `legend${i}` } };
} else {
legend.encode.legend = Object.assign({}, legend.encode.legend, {
name: `legend${i}`
});
}
});
}
}
function mergeDuplicatedAxes(vegaAxes) {
if (!vegaAxes || vegaAxes.length <= 0) {
return [];
}
let axesScales = vegaAxes.filter((a) => a.grid).map((a) => a.scale);
return d3
.rollups(
vegaAxes,
(axes) => {
let axisWithGrid = axes.find((a) => a.grid);
let axisWithoutGrid = { ...axes.find((a) => !a.grid) };
if (axisWithGrid) {
axisWithoutGrid.grid = true;
if (axisWithGrid.gridScale) {
axisWithoutGrid.gridScale = axisWithGrid.gridScale;
}
axisWithoutGrid.zindex = 0;
}
return axisWithoutGrid;
},
(axis) => axis.scale
)
.map((d) => d[1])
.sort(
(a, b) => axesScales.indexOf(a.scale) - axesScales.indexOf(b.scale)
);
}
let vgSpec = (vl == 4 ? vl4 : vl5).compile(area_spec_new_format(reserves, true /* params */, w)).spec; // vl5 at Least gives tooltips
vgSpec.axes = mergeDuplicatedAxes(vgSpec.axes);
appendNamesOnGuides(vgSpec);
// TODO my custom patches
vgSpec.axes[0].labelAngle = 0;
vgSpec.axes[0].labelAlign = 'center';
vgSpec.axes[0].labelBaseline = 'top'; // where did these get set ?!
vgSpec.scales = [
...(vgSpec.scales ? vgSpec.scales : []),
{
name: "formula_shadow",
type: "ordinal",
domain: ["premiums", "claims", ...(reserves>0 ? [reserves == 2 ? "Δ fut. losses reserve" : "Δ reserve"] :[]), ...(reserves>1 ? ["Δ % expd. future claims reserve"] :[])],
range: ["steelblue", "darkorange", ...(reserves>0 ? ["yellow"] :[]), ...(reserves>1 ? ["#f1c0e8"] :[])]
},
{
name: "profit_category_shadow",
type: "ordinal",
domain: ["loss", "-", "profit"],
range: ["red", "lightblue", "green"],
interpolate: "hcl"
}
];
vgSpec.legends = [
...(vgSpec.legends ? vgSpec.legends : []),
{
orient: 'top',//reserves == 2 ? 'bottom' : "top",
padding: 2,
strokeColor: reserves == 2 ? 'transparent' : "lightgrey",
title: "cashflows",
fill: "formula_shadow",
direction: reserves == 2 ? "vertical" : 'horizontal',
symbolType: "square",
encode: {
legend: { name: "legend_0" },
symbols: {
update: { opacity: { value: 1 } }
}
}
},
...(reserves < 2
? [
{
orient: "top",
padding: 2,
strokeColor: "lightgrey",
title: "Σ cashflows",
fill: "profit_category_shadow",
direction: "horizontal",
symbolType: "circle",
encode: {
legend: { name: "legend_1" },
symbols: {
update: { stroke: { value: "black" } }
}
}
}] : [])
];
vgSpec.marks = [
{
name: "bgtop",
type: "rect",
encode: {
update: {
fill: { value: "lightgreen" },
x: { scale: "x", field: "year_in" },
width: { signal: "width" },
//width: { scale: "x", signal: "domain('x')[1]" },
y: { scale: "y", value: 0 },
y2: { scale: "y", signal: "domain('y')[1]" },
opacity: { value: 0.2 }
}
}
},
{
name: "bgbottom",
type: "rect",
encode: {
update: {
fill: { value: "orange" },
x: { scale: "x", field: "year_in" },
width: { signal: "width" },
y2: { scale: "y", value: 0 },
y: { scale: "y", signal: "domain('y')[0]" },
opacity: { value: 0.2 }
}
}
},
{
name: "minus_annotation",
type: "text",
encode: {
update: {
x: { signal: "width*1/3" },
y: { signal: "height*4/5" },
text: { value: "➖" }, // 💸
fontSize: { value: 30 },
align: { value: "center" },
baseline: { value: "middle" },
opacity: { value: 0.7 }
}
}
},
{
name: "plus_annotation",
type: "text",
encode: {
update: {
x: { signal: "width*1/3" },
y: { signal: "height*1/5" },
text: { value: "➕" }, // 💰
fontSize: { value: 30 },
align: { value: "center" },
baseline: { value: "middle" },
opacity: { value: 0.7 }
}
}
},
{
name: "zero_rule",
type: "rule",
encode: {
update: {
x: { signal: "0" },
y: { signal: "0", scale: "y" },
x2: { signal: "width" },
//y2: { signal: "height*2/5" },
stroke: { signal: "'gray'" },
strokeWidth: { signal: "2" }
}
}
},
...vgSpec.marks
];
return vgSpec;
}
model init stuff
Code
Code
fs = ({
"entrypoint.cul.js": await FileAttachment('./cul/portfolio.cul.js').text(), // not actually using this ...
"./policy.cul.js": await FileAttachment('./cul/policy.cul.js').text()
})
introspection = await calculang.introspection('entrypoint.cul.js', fs)
compiled = await calculang.compile_new('entrypoint.cul.js', fs, introspection) // ?
esm = compiled[0]
bundle = calculang.bundleIntoOne(compiled, introspection, true)
u = URL.createObjectURL(new Blob([bundle], { type: "text/javascript" }))
console.log(`creating ${u}`)
Code
Code
projection0_ = helpers.calcudata({
models: [model],
outputs: ["premiums", "mclaims", "profit", "mΔreserve"],
input_domains: { year_in: _.range(0, 20 + 0.1, 1) },
input_cursors: [cursor, cursor] // for update in enter/exit/update pattern to match
//pivot: true
})
//.filter((d) => d.formula == "profit" || Math.abs(d.value) > 0.001)
projection0 = aq
.from(projection0_)
.groupby(["year_in", "formula"])
.orderby("input_cursor_id")
.derive({ A: (d) => aq.op.lag(d.value, 1, 0), B: (d) => d.value })
.filter((d) => d.input_cursor_id == 1)
.derive({
keep: (
d // =IF(C6<0,MIN(0,MAX(D6,C6)),IF(C6>0,MAX(0,MIN(D6,C6))))
) => {
if (d.A < 0) return Math.min(0, Math.max(d.A, d.B));
return Math.max(0, Math.min(d.A, d.B));
},
add: (d) => Math.max(0, d.B - d.A),
subtract: (d) => Math.max(0, d.A - d.B)
})
.fold(["A", "B", "keep", "add", "subtract"], { as: ["partition", "value"] })
projection1_ = helpers.calcudata({
models: [model],
outputs: ["premiums", "mclaims", "profit", "mΔreserve"],
input_domains: { year_in: _.range(0, 20 + 0.1, 1) },
input_cursors: [cursor, { ...cursor, limit_in: 0 }] // for update in enter/exit/update pattern to match
//pivot: true
})
//.filter((d) => d.formula == "profit" || Math.abs(d.value) > 0.001)
projection1 = aq
.from(projection1_)
.groupby(["year_in", "formula"])
.orderby("input_cursor_id")
.derive({ A: (d) => aq.op.lag(d.value, 1, 0), B: (d) => d.value })
.filter((d) => d.input_cursor_id == 1)
.derive({
keep: (
d // =IF(C6<0,MIN(0,MAX(D6,C6)),IF(C6>0,MAX(0,MIN(D6,C6))))
) => {
if (d.A < 0) return Math.min(0, Math.max(d.A, d.B));
return Math.max(0, Math.min(d.A, d.B));
},
add: (d) => Math.max(0, d.B - d.A),
subtract: (d) => Math.max(0, d.A - d.B)
})
.fold(["A", "B", "keep", "add", "subtract"], { as: ["partition", "value"] })
projection2_ = helpers.calcudata({
models: [model],
outputs: ["premiums", "mclaims", "profit", "mΔreserve", "mΔadverse_reserve"],
input_domains: { year_in: _.range(0, 20 + 0.1, 1) },
input_cursors: [{...cursor,limit_in:0}, { ...cursor, limit_in:0,adverse_reserve_factor_in: slider }] // for update in enter/exit/update pattern to match
//pivot: true
})
//.filter((d) => d.formula == "profit" || Math.abs(d.value) > 0.001)
projection2 = aq
.from(projection2_)
.groupby(["year_in", "formula"])
.orderby("input_cursor_id")
.derive({ A: (d) => aq.op.lag(d.value, 1, 0), B: (d) => d.value })
.filter((d) => d.input_cursor_id == 1)
.derive({
keep: (
d // =IF(C6<0,MIN(0,MAX(D6,C6)),IF(C6>0,MAX(0,MIN(D6,C6))))
) => {
if (d.A < 0) return Math.min(0, Math.max(d.A, d.B));
return Math.max(0, Math.min(d.A, d.B));
},
add: (d) => Math.max(0, d.B - d.A),
subtract: (d) => Math.max(0, d.A - d.B)
})
.fold(["A", "B", "keep", "add", "subtract"], { as: ["partition", "value"] })
cursor = ({ premiums_fn_in:() => premiums, claims_fn_in:() => claims, point_id_in: 0, adverse_reserve_factor_in:0, limit_in:10000 })
Code
spec_profile = {
let s = Object.assign({}, vgspec_profile({reserves:0,vl:5})); // , { data: { values: projection0 } }
s.data = s.data.map((d) =>
d.name == "projection" ? { name: "projection", values: [...projection0] } : d
);
return s;
}
spec0 = {
let s = JSON.parse(JSON.stringify(vgspec_profile({reserves:1, vl:4}))); // structuredClone(spec); //Object.assign({}, spec); // , { data: { values: projection0 } }
s.data = s.data.map((d) =>
d.name == "projection"
? { name: "projection", values: [...projection0] }
: d
);
s.marks = [...s.marks];
s.marks[5].encode.update.opacity = { value: 1 };
return s;
}
spec1 = {
let s = Object.assign({}, vgspec_profile({reserves:1, vl:4})); // , { data: { values: projection0 } }
s.data = s.data.map((d) =>
d.name == "projection"
? { name: "projection", values: [...projection1] }
: d
);
return s;
}
gemini = require("https://cdn.jsdelivr.net/gh/declann/gemini@1c2e679/gemini.web.js") // pinned
gem = ({
//"meta": {"name": "All at once"},
staggerings: [
{
name: "a",
by: "year_in",
order: /*"ascending", //*/ step ? "ascending" : "descending",
overlap: 0 //0.7
}
],
timeline: {
concat: [
...(step
? [
{
component: { mark: "layer_0_marks" }, // rect
change: {
data: false,
//data: { keys: ["year_in", "formula"] },
encode: {
update: { y: false, y2: false },
enter: false,
exit: false
}
},
timing: { duration: 300 }
}
]
: []),
{
component: { mark: "layer_0_marks" }, // rect
change: {
//data: { keys: ["year_in", "formula"] },
encode: {
update: { opacity: false }
// enter: true, // for now
//exit: true
}
},
timing: { staggering: "a", duration: { ratio: step == 1 ? 1 : 2 } }
},
{sync: [{
component: { mark: "layer_1_marks" },
timing: { duration: { ratio: 0.7 } }
},
{
component: { mark: "layer_2_marks" },
timing: { duration: { ratio: 0.7 } }
},
...(!step
? [
{
component: { mark: "layer_0_marks" }, // rect
change: {
data: false,
//data: { keys: ["year_in", "formula"] },
encode: {
update: { y: false, y2: false },
enter: false,
exit: false
}
},
timing: { duration: 1500 }
}
]
: [])]}
]
},
totalDuration: step == 1 ? 2200 : 1000
})
mapped = [spec0, spec1] //.map(gemini.vl2vg4gemini)
setV0 = (v) => (mutable v0 = v)
mutable v0 = 0
mutable flicks = 0
setFlicks = v => {mutable flicks = v}
v1 = step
viewof step = Inputs.range([0, 1], {
label: "step ▶️",
step: 1,
value: 0
})
{
if (step != mutable v0) {
console.log("RUNNING");
document.querySelectorAll('input.actionCheckbox').forEach(d => {
d.disabled = true;
})
const animation = await gemini.animate(mapped[v0], mapped[step], gem);
await animation.play("#tansition-el");
setV0(step);
console.log("done");
setFlicks(flicks+1)
if (window.plausible && flicks < 3) {
window.plausible('act-terms-i reserves 1',
{ props: {flicks: flicks}})
}
if (mutable e == false) {
mutable e = true;
document.getElementById('i').style.visibility = 'visible'//opacity = 1
document.getElementById('i').classList.remove('introduceReverse')// = 1//opacity = 1
document.getElementById('i').classList.add('introduce')// = 1//opacity = 1
} else {
mutable e = false
//document.getElementById('i').style.visibility = 'hidden'//opacity = 1
document.getElementById('i').classList.remove('introduce')// = 1//opacity = 1
document.getElementById('i').classList.add('introduceReverse')// = 1//opacity = 1
}
set(viewof button, {value: viewof button.value.value, disabled:false})
document.querySelectorAll('input.actionCheckbox').forEach(d => {
d.disabled = false;
})
}
}
Code
{
if (slider > .179) {
if (window.plausible && !sliderActivated)
window.plausible('act-terms-i slider activated')
mutable sliderActivated = 1
document.getElementsByClassName('slider-box')[0].style.animation = 'none'
document.getElementById('jj2').style.visibility = 'visible'//opacity = 1
document.getElementById('jj3').style.visibility = 'visible'//opacity = 1
document.getElementById('jj').style.visibility = 'visible'//opacity = 1
document.getElementById('jj2').classList.add('introduce')// = 1//opacity = 1
document.getElementById('jj3').classList.add('introduce-jj3')// = 1//opacity = 1
}
}