craft-library 0.1.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -3
- package/dist/card-load.js +2 -0
- package/dist/card-mount.js +32 -0
- package/dist/card-visa.png +0 -0
- package/dist/card.cdn.js +593 -0
- package/dist/card.js +596 -0
- package/dist/cdn.js +4 -3
- package/dist/craft.js +108 -2
- package/dist/elements.js +34 -2
- package/dist/index.js +29 -1
- package/dist/piggy-load.js +2 -0
- package/dist/piggy-mount.js +27 -0
- package/dist/piggy.cdn.js +633 -0
- package/dist/piggy.js +633 -0
- package/package.json +13 -4
package/dist/card.js
ADDED
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
// Iridescent card · Craft Library, by Sara Lobo
|
|
2
|
+
// A metal credit card in 3D: brushed iridescent plate, a fine struck frame along the edge, a chip and the Visa mark.
|
|
3
|
+
// It arrives with a slow half turn, from its back to its face, and a tap turns it over to show the numbers.
|
|
4
|
+
//
|
|
5
|
+
// import { mountCard } from './card.js';
|
|
6
|
+
// const card = mountCard(document.querySelector('#wallet')); // fills the element, card centred
|
|
7
|
+
// card.turn(); card.show('back'); card.play(); card.destroy();
|
|
8
|
+
//
|
|
9
|
+
// Needs three.js 0.169 (imported from jsDelivr below) and the card-visa.png file next to this one.
|
|
10
|
+
import * as THREE from 'three';
|
|
11
|
+
|
|
12
|
+
const DEFAULTS = {
|
|
13
|
+
name: 'Sara Lobo', number: ['4532', '8890', '1274', '3306'], expiry: '08/30', cvv: '412', // fails Luhn on purpose
|
|
14
|
+
logo: new URL('card-visa.png', import.meta.url).href, // the official Visa mark (Wikimedia Commons, public domain)
|
|
15
|
+
size: 0.72, // the card's height as a share of the element's height (it also keeps 12% clear at the sides)
|
|
16
|
+
entrance: true, // the half turn when it mounts
|
|
17
|
+
interactive: true, // tap, drag and keys turn it over
|
|
18
|
+
transparent: true, // the page shows around the card; false paints the ground black
|
|
19
|
+
label: 'Cartão de crédito. Pressione para virar e ver o verso.',
|
|
20
|
+
manual: false, // no loop of its own: call frame(ms) to draw (used to film it)
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// ─── Geometry: an ID-1 card (85.6 × 53.98 mm, r 3.18 mm), stood upright ──────
|
|
24
|
+
const H = 3.4, W = H * 53.98 / 85.6, R = H * 3.18 / 85.6, T = 0.056;
|
|
25
|
+
const BEVEL = 0.016; // a fully rounded rim, so the edge catches a line of silver
|
|
26
|
+
// The relief: a slim frame along each face's edge, a hair higher than the plate, ending in a fine chamfer.
|
|
27
|
+
const FRAME = { inset: 0.05, chamfer: 0.007, height: 0.0035 };
|
|
28
|
+
const REST_X = 0.05, REST_Y = -0.26; // a three-quarter pose: the right edge in view
|
|
29
|
+
const CW = W - 2 * BEVEL, CH = H - 2 * BEVEL;
|
|
30
|
+
|
|
31
|
+
// ─── Motion ─────────────────────────────────────────────────────────────────
|
|
32
|
+
class Spring {
|
|
33
|
+
constructor(value = 0, { stiffness = 220, zeta = 0.7 } = {}) {
|
|
34
|
+
this.x = value; this.v = 0; this.target = value;
|
|
35
|
+
this.k = stiffness; this.c = 2 * zeta * Math.sqrt(stiffness);
|
|
36
|
+
}
|
|
37
|
+
step(dt) {
|
|
38
|
+
const n = Math.max(1, Math.ceil(dt * 240)), h = dt / n;
|
|
39
|
+
for (let i = 0; i < n; i++) { this.v += (-this.k * (this.x - this.target) - this.c * this.v) * h; this.x += this.v * h; }
|
|
40
|
+
return this.x;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// The entrance: each channel eases from its offset to the rest pose over its own time, and both curves end with
|
|
44
|
+
// no speed and no acceleration, so the card comes to rest without a step.
|
|
45
|
+
const EASE = {
|
|
46
|
+
out: (s) => 1 - (1 - s) ** 3, // already moving: it fades in as it comes forward
|
|
47
|
+
glide: (s) => s * s * (6 - 8 * s + 3 * s * s), // from rest, fastest at a third, then a long soft slowdown
|
|
48
|
+
};
|
|
49
|
+
const ENTRANCE = {
|
|
50
|
+
fade: { from: 1, dur: 0.8 }, // out of the dark
|
|
51
|
+
z: { from: -1.6, dur: 1.6 }, // from just behind its place
|
|
52
|
+
spin: { from: -Math.PI, dur: 1.6, ease: 'glide' }, // showing its back, a slow half turn to the face
|
|
53
|
+
};
|
|
54
|
+
const settle = (t, { from, dur, ease = 'out' }) => from * (1 - EASE[ease](Math.min(1, Math.max(0, t / dur))));
|
|
55
|
+
const ENTRANCE_END = 1.6;
|
|
56
|
+
|
|
57
|
+
// ─── The light on the plate ─────────────────────────────────────────────────
|
|
58
|
+
// A lilac pool in the top-left corner cut by a soft diagonal, a ribbon of teal green just under that edge, royal
|
|
59
|
+
// blue down the right, sinking to ink at the foot. u runs left → right, v top → bottom.
|
|
60
|
+
const ss = (a, b, x) => { const t = Math.min(1, Math.max(0, (x - a) / (b - a))); return t * t * (3 - 2 * t); };
|
|
61
|
+
const mix3 = (a, b, t) => a.map((x, i) => x + (b[i] - x) * t);
|
|
62
|
+
const LILAC = [152, 128, 204], PERIWINKLE = [108, 120, 198], TEAL = [42, 152, 138], DEEP = [20, 38, 104],
|
|
63
|
+
ROYAL = [50, 62, 158], ROYAL_D = [24, 30, 100], DUSK = [12, 15, 54], INK = [3, 5, 14];
|
|
64
|
+
function lightField(u, v) {
|
|
65
|
+
const d = v - (0.26 + 0.76 * u);
|
|
66
|
+
const lit = 1 - ss(-0.12, 0.18, d);
|
|
67
|
+
const dim = (1 - 0.25 * ss(0.15, 0.6, u)) * (1 - 0.4 * ss(0.35, 0.85, v)) * (1 - 0.1 * ss(0.14, 0, v));
|
|
68
|
+
const upper = mix3(LILAC, PERIWINKLE, Math.min(1, ss(0.1, 0.5, u) * 0.8 + ss(0.25, 0.7, v) * 0.5)).map((x) => x * dim);
|
|
69
|
+
const lower = mix3(mix3(TEAL, DEEP, ss(0.04, 0.42, d)), INK, ss(0.3, 0.6, d));
|
|
70
|
+
let right = mix3(ROYAL, ROYAL_D, ss(0.65, 1.0, u));
|
|
71
|
+
right = mix3(mix3(right, DUSK, ss(0.6, 0.82, v)), INK, ss(0.8, 0.96, v));
|
|
72
|
+
const foot = 1 - 0.86 * ss(0.6, 0.94, v);
|
|
73
|
+
return mix3(mix3(lower, upper, lit), right, ss(0.42, 0.86, u)).map((x) => x * foot);
|
|
74
|
+
}
|
|
75
|
+
// Painted wider than the card (−0.35…1.35) so the light can slide when the card turns.
|
|
76
|
+
const FIELD_PAD = 0.35, SHIFT_GAIN = 0.8;
|
|
77
|
+
const TW = 1024, TH = Math.round(TW * H / W), U = TW / 100; // print canvas, and 1% of the card's width
|
|
78
|
+
const JOST = '"Jost", "Futura", ui-sans-serif, system-ui, sans-serif';
|
|
79
|
+
const INK_PRINT = 'rgba(242,240,250,0.94)', MUTED_PRINT = 'rgba(222,220,240,0.5)';
|
|
80
|
+
|
|
81
|
+
function rng(seed) { return () => ((seed = (seed * 16807) % 2147483647) - 1) / 2147483646; }
|
|
82
|
+
function canvas2d(w = TW, h = TH) {
|
|
83
|
+
const c = document.createElement('canvas'); c.width = w; c.height = h;
|
|
84
|
+
return [c, c.getContext('2d')];
|
|
85
|
+
}
|
|
86
|
+
function fieldCanvas() {
|
|
87
|
+
const fw = 160, fh = Math.round(fw * H / W), [c, g] = canvas2d(fw, fh), img = g.createImageData(fw, fh);
|
|
88
|
+
for (let j = 0; j < fh; j++) for (let i = 0; i < fw; i++) {
|
|
89
|
+
const u = -FIELD_PAD + (1 + 2 * FIELD_PAD) * (i + 0.5) / fw, v = -FIELD_PAD + (1 + 2 * FIELD_PAD) * (j + 0.5) / fh;
|
|
90
|
+
const [r, gg, b] = lightField(u, v), k = (j * fw + i) * 4;
|
|
91
|
+
img.data[k] = r; img.data[k + 1] = gg; img.data[k + 2] = b; img.data[k + 3] = 255;
|
|
92
|
+
}
|
|
93
|
+
g.putImageData(img, 0, 0);
|
|
94
|
+
return c;
|
|
95
|
+
}
|
|
96
|
+
// The brushed grooves: thin bright lines on strips of slightly different shade, constant down the card.
|
|
97
|
+
// R holds the line, G the shade of the strip it sits on.
|
|
98
|
+
function grooveCanvas(seed) {
|
|
99
|
+
const r = rng(seed), marks = [];
|
|
100
|
+
for (let x = 6 + r() * 10; x < TW; x += 15 + r() * 20) {
|
|
101
|
+
marks.push([x, 0.9 + r() * 1.4, 0.35 + r() * 0.65]);
|
|
102
|
+
if (r() < 0.22) marks.push([x + 4 + r() * 3, 0.8 + r(), 0.25 + r() * 0.4]);
|
|
103
|
+
}
|
|
104
|
+
const [c, g] = canvas2d(TW, 1), img = g.createImageData(TW, 1);
|
|
105
|
+
let m = 0, shade = 0;
|
|
106
|
+
for (let i = 0; i < TW; i++) {
|
|
107
|
+
if (m < marks.length && i >= marks[m][0]) { shade = (r() - 0.5) * 0.12; m++; }
|
|
108
|
+
let line = 0;
|
|
109
|
+
for (const [mx, w, s] of marks) { const d = (i - mx) / w; if (d > -4 && d < 4) line += Math.exp(-d * d) * s; }
|
|
110
|
+
img.data[i * 4] = Math.min(1, line) * 255; img.data[i * 4 + 1] = (0.5 + shade * 4) * 255; img.data[i * 4 + 3] = 255;
|
|
111
|
+
}
|
|
112
|
+
g.putImageData(img, 0, 0);
|
|
113
|
+
return { canvas: c, data: img.data };
|
|
114
|
+
}
|
|
115
|
+
// Roughness in G, metalness in B (three.js reads them from these channels).
|
|
116
|
+
function paintSurface(g, grooves, roughBase, metal) {
|
|
117
|
+
for (let x = 0; x < TW; x++) {
|
|
118
|
+
g.fillStyle = `rgb(0,${Math.round(255 * (roughBase - grooves[x * 4] / 255 * 0.16))},${Math.round(metal * 255)})`;
|
|
119
|
+
g.fillRect(x, 0, 1, TH);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// Print goes on a transparent layer over the plate (a) and into the surface map (b), where it is smoother and more
|
|
123
|
+
// metallic than the brushed plate, so it glints as the card turns.
|
|
124
|
+
function printer(a, b) {
|
|
125
|
+
return (str, x, y, { size, weight = 300, tracking = 0, fill, align = 'left' }) => {
|
|
126
|
+
for (const [g, style] of [[a, fill], [b, 'rgb(0,42,235)']]) {
|
|
127
|
+
g.font = `${weight} ${size * U}px ${JOST}`;
|
|
128
|
+
g.letterSpacing = `${tracking * size * U}px`; // tracking in em
|
|
129
|
+
g.textAlign = align;
|
|
130
|
+
g.fillStyle = style;
|
|
131
|
+
g.fillText(str, x, y);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function tinted(img, w, h, fill) {
|
|
136
|
+
const [c, g] = canvas2d(Math.round(w), Math.round(h));
|
|
137
|
+
g.drawImage(img, 0, 0, c.width, c.height);
|
|
138
|
+
g.globalCompositeOperation = 'source-in';
|
|
139
|
+
g.fillStyle = fill; g.fillRect(0, 0, c.width, c.height);
|
|
140
|
+
return c;
|
|
141
|
+
}
|
|
142
|
+
function roundedRect(w, h, r) {
|
|
143
|
+
const s = new THREE.Shape(), x = -w / 2, y = -h / 2;
|
|
144
|
+
s.moveTo(x + r, y);
|
|
145
|
+
s.lineTo(x + w - r, y); s.quadraticCurveTo(x + w, y, x + w, y + r);
|
|
146
|
+
s.lineTo(x + w, y + h - r); s.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
|
|
147
|
+
s.lineTo(x + r, y + h); s.quadraticCurveTo(x, y + h, x, y + h - r);
|
|
148
|
+
s.lineTo(x, y + r); s.quadraticCurveTo(x, y, x + r, y);
|
|
149
|
+
return s;
|
|
150
|
+
}
|
|
151
|
+
function chipCanvas() {
|
|
152
|
+
const [c, g] = canvas2d(256, 320);
|
|
153
|
+
const grd = g.createLinearGradient(0, 0, 256, 320);
|
|
154
|
+
grd.addColorStop(0, '#d4d3d6'); grd.addColorStop(0.55, '#a7a6ad'); grd.addColorStop(1, '#bebdc3');
|
|
155
|
+
g.fillStyle = grd; g.fillRect(0, 0, 256, 320);
|
|
156
|
+
g.strokeStyle = 'rgba(78,78,86,0.7)'; g.lineWidth = 4; g.lineCap = 'round'; g.lineJoin = 'round';
|
|
157
|
+
g.beginPath();
|
|
158
|
+
g.moveTo(128, 0); g.lineTo(128, 92); // centre spine
|
|
159
|
+
g.moveTo(128, 228); g.lineTo(128, 320);
|
|
160
|
+
g.roundRect(84, 92, 88, 136, 18); // the die pad
|
|
161
|
+
g.moveTo(0, 112); g.lineTo(84, 112); g.moveTo(172, 112); g.lineTo(256, 112);
|
|
162
|
+
g.moveTo(0, 208); g.lineTo(84, 208); g.moveTo(172, 208); g.lineTo(256, 208);
|
|
163
|
+
g.moveTo(84, 160); g.lineTo(40, 160); g.quadraticCurveTo(30, 160, 30, 170); g.lineTo(30, 208);
|
|
164
|
+
g.moveTo(172, 160); g.lineTo(226, 160); g.quadraticCurveTo(236, 160, 236, 150); g.lineTo(236, 112);
|
|
165
|
+
g.stroke();
|
|
166
|
+
return c;
|
|
167
|
+
}
|
|
168
|
+
// The typeface of the print: Jost, from Google Fonts, added to the page once.
|
|
169
|
+
function loadJost() {
|
|
170
|
+
if (!document.querySelector('link[data-craft-jost]')) {
|
|
171
|
+
const l = document.createElement('link');
|
|
172
|
+
l.rel = 'stylesheet'; l.dataset.craftJost = '';
|
|
173
|
+
l.href = 'https://fonts.googleapis.com/css2?family=Jost:wght@300;400&display=swap';
|
|
174
|
+
document.head.appendChild(l);
|
|
175
|
+
}
|
|
176
|
+
return Promise.race([
|
|
177
|
+
Promise.all(['300 40px', '400 40px'].map((f) => document.fonts.load(`${f} "Jost"`))),
|
|
178
|
+
new Promise((r) => setTimeout(r, 3000)),
|
|
179
|
+
]).catch(() => {});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function mountCard(container, options = {}) {
|
|
183
|
+
const o = { ...DEFAULTS, ...options };
|
|
184
|
+
const reduceMq = matchMedia('(prefers-reduced-motion: reduce)');
|
|
185
|
+
let reduce = reduceMq.matches;
|
|
186
|
+
const onReduce = (e) => { reduce = e.matches; };
|
|
187
|
+
reduceMq.addEventListener('change', onReduce);
|
|
188
|
+
|
|
189
|
+
// ─── Renderer ─────────────────────────────────────────────────────────────
|
|
190
|
+
const host = document.createElement('div');
|
|
191
|
+
host.className = 'craft-card';
|
|
192
|
+
host.style.cssText = 'position:absolute;inset:0;outline:none;touch-action:none;-webkit-tap-highlight-color:transparent';
|
|
193
|
+
if (getComputedStyle(container).position === 'static') container.style.position = 'relative';
|
|
194
|
+
if (o.interactive) { host.setAttribute('role', 'button'); host.tabIndex = 0; host.setAttribute('aria-label', o.label); }
|
|
195
|
+
container.appendChild(host);
|
|
196
|
+
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: o.transparent, powerPreference: 'high-performance', preserveDrawingBuffer: o.manual });
|
|
197
|
+
renderer.setPixelRatio(Math.min(devicePixelRatio, 1.75));
|
|
198
|
+
renderer.setClearColor(0x000000, o.transparent ? 0 : 1);
|
|
199
|
+
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
|
200
|
+
// No tone mapping: Neutral's toe pulls the dark violets toward saturated blue.
|
|
201
|
+
renderer.toneMapping = THREE.NoToneMapping;
|
|
202
|
+
const canvas = renderer.domElement;
|
|
203
|
+
canvas.style.cssText = 'display:block;width:100%;height:100%;transition:opacity 160ms ease';
|
|
204
|
+
host.appendChild(canvas);
|
|
205
|
+
const scene = new THREE.Scene();
|
|
206
|
+
const camera = new THREE.PerspectiveCamera(26, 1, 0.1, 100);
|
|
207
|
+
const aniso = renderer.capabilities.getMaxAnisotropy();
|
|
208
|
+
const disposables = [];
|
|
209
|
+
const keep = (x) => { disposables.push(x); return x; };
|
|
210
|
+
|
|
211
|
+
// ─── Studio ───────────────────────────────────────────────────────────────
|
|
212
|
+
// The colour of the plate comes from its own light field, so the sky stays black where the face looks at rest.
|
|
213
|
+
// The panels show when the card turns, and give the rim its line of silver.
|
|
214
|
+
function bake(env) {
|
|
215
|
+
const pmrem = new THREE.PMREMGenerator(renderer);
|
|
216
|
+
const rt = pmrem.fromScene(env, 0.02);
|
|
217
|
+
pmrem.dispose();
|
|
218
|
+
env.traverse((n) => { if (n.isMesh) { n.geometry.dispose(); n.material.dispose(); } });
|
|
219
|
+
return keep(rt).texture;
|
|
220
|
+
}
|
|
221
|
+
function panels(env, list) {
|
|
222
|
+
for (const [w, h, intensity, tint, pos] of list) {
|
|
223
|
+
const m = new THREE.Mesh(new THREE.PlaneGeometry(w, h), new THREE.MeshBasicMaterial({ color: new THREE.Color(tint).multiplyScalar(intensity), side: THREE.DoubleSide }));
|
|
224
|
+
m.position.set(...pos); m.lookAt(0, 0, 0);
|
|
225
|
+
env.add(m);
|
|
226
|
+
}
|
|
227
|
+
return env;
|
|
228
|
+
}
|
|
229
|
+
scene.environment = bake(panels(new THREE.Scene(), [
|
|
230
|
+
[4, 3, 1.8, 0xffffff, [-5, 5, 4]], // key, top left in front
|
|
231
|
+
[7, 9, 1.4, 0xf2f2ff, [6, 0.5, -1.5]], // right side: the silver of the edge
|
|
232
|
+
[8, 2.5, 0.9, 0xffffff, [0, 6, 1]], // top rim
|
|
233
|
+
[3, 6, 0.3, 0xdfe2ff, [-7, 0, -1]], // dim left
|
|
234
|
+
]));
|
|
235
|
+
// The edge gets a brighter studio of its own, with an aqua and a violet strip its metal picks up as it turns.
|
|
236
|
+
const rimScene = new THREE.Scene();
|
|
237
|
+
rimScene.add(new THREE.Mesh(new THREE.SphereGeometry(20, 32, 16), new THREE.ShaderMaterial({
|
|
238
|
+
side: THREE.BackSide, depthWrite: false,
|
|
239
|
+
vertexShader: 'varying vec3 vP; void main(){ vP = normalize(position); gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.); }',
|
|
240
|
+
fragmentShader: 'varying vec3 vP; void main(){ gl_FragColor = vec4(mix(vec3(.012,.012,.02), vec3(.07,.07,.09), vP.y * .5 + .5), 1.); }',
|
|
241
|
+
})));
|
|
242
|
+
const rimEnv = bake(panels(rimScene, [
|
|
243
|
+
[4, 4, 0.5, 0xf0eeff, [-4, 1.2, 4.6]], // front fill, brighter toward the top left
|
|
244
|
+
[4, 4, 0.16, 0xc8d4ff, [-1.2, -2.6, 5.2]], // and fading toward the bottom
|
|
245
|
+
[4, 3, 2.4, 0xffffff, [-5, 5, 4]], // key
|
|
246
|
+
[7, 9, 1.6, 0xeef8ff, [6, 0.5, -1.5]], // right side
|
|
247
|
+
[8, 2.5, 1.2, 0xffffff, [0, 6, 1]], // top
|
|
248
|
+
[5, 1.2, 1.1, 0x6ff0dc, [3, -3.5, 4]], // aqua
|
|
249
|
+
[5, 1.2, 1.0, 0x9a78ff, [-5, -3, 1]], // violet
|
|
250
|
+
]));
|
|
251
|
+
|
|
252
|
+
// ─── Textures, painted once ───────────────────────────────────────────────
|
|
253
|
+
function texture(c, srgb) {
|
|
254
|
+
const t = keep(new THREE.CanvasTexture(c));
|
|
255
|
+
t.colorSpace = srgb ? THREE.SRGBColorSpace : THREE.NoColorSpace;
|
|
256
|
+
t.anisotropy = aniso;
|
|
257
|
+
return t;
|
|
258
|
+
}
|
|
259
|
+
const field = texture(fieldCanvas(), true);
|
|
260
|
+
const frontG = grooveCanvas(7), backG = grooveCanvas(31);
|
|
261
|
+
const frontGrooves = texture(frontG.canvas, false), backGrooves = texture(backG.canvas, false);
|
|
262
|
+
let logo = null;
|
|
263
|
+
const [fc, fg] = canvas2d(), [fsC, fsG] = canvas2d();
|
|
264
|
+
function paintFront() {
|
|
265
|
+
fg.clearRect(0, 0, TW, TH);
|
|
266
|
+
paintSurface(fsG, frontG.data, 0.32, 0.6);
|
|
267
|
+
if (!logo) return;
|
|
268
|
+
const w = 0.24 * TW, h = w * logo.naturalHeight / logo.naturalWidth;
|
|
269
|
+
const x = 0.865 * TW - w, y = 0.918 * TH - h;
|
|
270
|
+
fg.drawImage(tinted(logo, w, h, 'rgba(246,246,252,0.97)'), x, y);
|
|
271
|
+
fsG.drawImage(tinted(logo, w, h, 'rgb(0,42,235)'), x, y); // smooth and metallic, so it glints
|
|
272
|
+
}
|
|
273
|
+
// The back: a slim glossy stripe, then one column of small type with room around it.
|
|
274
|
+
const stripe = { x: 0.075, w: 0.12 };
|
|
275
|
+
const [bc, bg] = canvas2d(), [bsC, bsG] = canvas2d();
|
|
276
|
+
function paintBack() {
|
|
277
|
+
bg.clearRect(0, 0, TW, TH);
|
|
278
|
+
const band = bg.createLinearGradient(stripe.x * TW, 0, (stripe.x + stripe.w) * TW, 0);
|
|
279
|
+
band.addColorStop(0, 'rgba(7,7,13,0.97)'); band.addColorStop(0.5, 'rgba(18,18,30,0.97)'); band.addColorStop(1, 'rgba(7,7,13,0.97)');
|
|
280
|
+
bg.fillStyle = band; bg.fillRect(stripe.x * TW, 0, stripe.w * TW, TH);
|
|
281
|
+
paintSurface(bsG, backG.data, 0.34, 0.6);
|
|
282
|
+
bsG.fillStyle = 'rgb(0,40,40)'; bsG.fillRect(stripe.x * TW, 0, stripe.w * TW, TH);
|
|
283
|
+
const print = printer(bg, bsG), x0 = (stripe.x + stripe.w + 0.085) * TW, col2 = x0 + 21 * U;
|
|
284
|
+
print(o.name.toUpperCase(), x0, 0.108 * TH, { size: 2.9, weight: 400, tracking: 0.24, fill: INK_PRINT });
|
|
285
|
+
const yN = 0.69 * TH;
|
|
286
|
+
print(`${o.number[0]} ${o.number[1]}`, x0, yN, { size: 5.2, tracking: 0.12, fill: INK_PRINT });
|
|
287
|
+
print(`${o.number[2]} ${o.number[3]}`, x0, yN + 7.6 * U, { size: 5.2, tracking: 0.12, fill: INK_PRINT });
|
|
288
|
+
const yL = 0.835 * TH;
|
|
289
|
+
print('VALID THRU', x0, yL, { size: 2.1, weight: 400, tracking: 0.2, fill: MUTED_PRINT });
|
|
290
|
+
print('CVV', col2, yL, { size: 2.1, weight: 400, tracking: 0.2, fill: MUTED_PRINT });
|
|
291
|
+
print(o.expiry, x0, yL + 4.9 * U, { size: 3.4, tracking: 0.12, fill: INK_PRINT });
|
|
292
|
+
print(o.cvv, col2, yL + 4.9 * U, { size: 3.4, tracking: 0.12, fill: INK_PRINT });
|
|
293
|
+
}
|
|
294
|
+
paintFront(); paintBack();
|
|
295
|
+
const frontDetail = texture(fc, true), frontSurf = texture(fsC, false);
|
|
296
|
+
const backDetail = texture(bc, true), backSurf = texture(bsC, false);
|
|
297
|
+
frontDetail.premultiplyAlpha = backDetail.premultiplyAlpha = true; // clean type edges in the mipmaps
|
|
298
|
+
// The back is seen through the card's −z side, so its u runs the other way.
|
|
299
|
+
for (const t of [backDetail, backSurf]) { t.wrapS = THREE.RepeatWrapping; t.repeat.x = -1; t.offset.x = 1; }
|
|
300
|
+
const repaint = () => { paintFront(); paintBack(); for (const t of [frontDetail, frontSurf, backDetail, backSurf]) t.needsUpdate = true; };
|
|
301
|
+
const logoReady = new Promise((resolve) => {
|
|
302
|
+
const img = new Image();
|
|
303
|
+
img.crossOrigin = 'anonymous';
|
|
304
|
+
img.src = o.logo;
|
|
305
|
+
img.decode().then(() => { logo = img; }, () => {}).then(resolve);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// ─── Materials ────────────────────────────────────────────────────────────
|
|
309
|
+
// The entrance fade: every material ends scaled by the same amount (an opacity fade on a transparent canvas,
|
|
310
|
+
// a fade from black on an opaque one).
|
|
311
|
+
const fadeU = { value: 1 };
|
|
312
|
+
function withFade(m) {
|
|
313
|
+
const own = m.onBeforeCompile;
|
|
314
|
+
m.onBeforeCompile = (sh, r) => {
|
|
315
|
+
own.call(m, sh, r);
|
|
316
|
+
sh.uniforms.uFade = fadeU;
|
|
317
|
+
sh.fragmentShader = sh.fragmentShader
|
|
318
|
+
.replace('#include <common>', '#include <common>\nuniform float uFade;')
|
|
319
|
+
.replace('#include <dithering_fragment>', '#include <dithering_fragment>\ngl_FragColor *= uFade;');
|
|
320
|
+
};
|
|
321
|
+
return keep(m);
|
|
322
|
+
}
|
|
323
|
+
// A physical metal whose colour is the light field, sampled with an offset that follows the face's angle, so
|
|
324
|
+
// turning the card slides the light across it. The grooves and the print stay put.
|
|
325
|
+
function plate(detail, surf, grooves) {
|
|
326
|
+
const shift = { value: new THREE.Vector2() };
|
|
327
|
+
const m = new THREE.MeshPhysicalMaterial({
|
|
328
|
+
map: detail, roughnessMap: surf, metalnessMap: surf,
|
|
329
|
+
roughness: 1, metalness: 1,
|
|
330
|
+
emissive: 0xffffff, emissiveIntensity: 1,
|
|
331
|
+
clearcoat: 0.5, clearcoatRoughness: 0.12,
|
|
332
|
+
iridescence: 0.25, iridescenceIOR: 1.4, iridescenceThicknessRange: [220, 520],
|
|
333
|
+
});
|
|
334
|
+
m.onBeforeCompile = (sh) => {
|
|
335
|
+
Object.assign(sh.uniforms, { uField: { value: field }, uGroove: { value: grooves }, uShift: shift });
|
|
336
|
+
sh.fragmentShader = sh.fragmentShader
|
|
337
|
+
.replace('#include <common>', `#include <common>
|
|
338
|
+
uniform sampler2D uField, uGroove;
|
|
339
|
+
uniform vec2 uShift;
|
|
340
|
+
// Height of the relief at a point of the face (card units, centred): the frame along the edge stands
|
|
341
|
+
// FRAME.height above the plate and steps down over the chamfer.
|
|
342
|
+
float frameHeight( vec2 p ) {
|
|
343
|
+
vec2 q = abs( p ) - vec2( ${(CW / 2 - (R - BEVEL)).toFixed(4)}, ${(CH / 2 - (R - BEVEL)).toFixed(4)} );
|
|
344
|
+
float inset = ${(R - BEVEL).toFixed(4)} - length( max( q, 0.0 ) ) - min( max( q.x, q.y ), 0.0 );
|
|
345
|
+
return ${FRAME.height.toFixed(4)} * ( 1.0 - smoothstep( ${(FRAME.inset - FRAME.chamfer).toFixed(4)}, ${(FRAME.inset + FRAME.chamfer).toFixed(4)}, inset ) );
|
|
346
|
+
}`)
|
|
347
|
+
.replace('#include <map_fragment>', `
|
|
348
|
+
vec4 detail = texture2D( map, vMapUv );
|
|
349
|
+
vec3 light = texture2D( uField, ( vMapUv + uShift + ${FIELD_PAD.toFixed(2)} ) / ${(1 + 2 * FIELD_PAD).toFixed(2)} ).rgb;
|
|
350
|
+
vec2 groove = texture2D( uGroove, vec2( vMapUv.x, 0.5 ) ).rg;
|
|
351
|
+
vec3 plateColor = light * ( 1.0 + ( groove.g - 0.5 ) * 0.5 ) + groove.r * 0.6 * ( light + 0.004 );
|
|
352
|
+
vec3 surfaceColor = plateColor * ( 1.0 - detail.a ) + detail.rgb;
|
|
353
|
+
// The relief: the frame's chamfer lit from the top left (the light leans as the card turns), so the inner
|
|
354
|
+
// walls facing it brighten and the others fall into shade, the way a struck edge reads.
|
|
355
|
+
vec2 cp = ( vMapUv - 0.5 ) * vec2( ${CW.toFixed(4)}, ${CH.toFixed(4)} );
|
|
356
|
+
const float reliefE = 0.0015;
|
|
357
|
+
vec2 slope = vec2( frameHeight( cp + vec2( reliefE, 0.0 ) ) - frameHeight( cp - vec2( reliefE, 0.0 ) ),
|
|
358
|
+
frameHeight( cp + vec2( 0.0, reliefE ) ) - frameHeight( cp - vec2( 0.0, reliefE ) ) ) / ( 2.0 * reliefE );
|
|
359
|
+
vec3 reliefN = normalize( vec3( -slope, 1.0 ) );
|
|
360
|
+
vec3 reliefL = normalize( vec3( -0.55 + uShift.x * 1.5, 0.65 + uShift.y * 1.5, 0.55 ) );
|
|
361
|
+
float relief = dot( reliefN, reliefL ) - reliefL.z;
|
|
362
|
+
surfaceColor = surfaceColor * ( 1.0 + relief * 1.4 ) + max( relief, 0.0 ) * vec3( 0.2, 0.22, 0.26 );
|
|
363
|
+
diffuseColor.rgb *= surfaceColor;
|
|
364
|
+
// The aqua beam: a band along the light's diagonal edge that runs ahead of the light when the card turns
|
|
365
|
+
// and lights up the grooves it crosses, the way brushed metal streaks.
|
|
366
|
+
vec2 gp = vMapUv + uShift * 2.4;
|
|
367
|
+
float gb = ( 1.0 - gp.y ) - ( 0.17 + 0.76 * gp.x );
|
|
368
|
+
float glint = exp( -gb * gb / 0.005 ) * ( 1.0 - smoothstep( 0.2, 0.62, gp.x ) ) * ( 0.15 + groove.r * 0.95 ) * ( 1.0 - detail.a );`)
|
|
369
|
+
.replace('#include <emissivemap_fragment>', 'totalEmissiveRadiance = totalEmissiveRadiance * surfaceColor + vec3( 0.72, 0.96, 1.0 ) * glint * 0.22;');
|
|
370
|
+
};
|
|
371
|
+
m.userData.shift = shift.value;
|
|
372
|
+
return withFade(m);
|
|
373
|
+
}
|
|
374
|
+
const frontMat = plate(frontDetail, frontSurf, frontGrooves), backMat = plate(backDetail, backSurf, backGrooves);
|
|
375
|
+
const edgeMat = withFade(new THREE.MeshStandardMaterial({ color: 0xc4c4d0, metalness: 1, roughness: 0.28, envMap: rimEnv }));
|
|
376
|
+
|
|
377
|
+
// ─── The card ─────────────────────────────────────────────────────────────
|
|
378
|
+
const capUV = {
|
|
379
|
+
generateTopUV(g, v, a, b, c) { return [a, b, c].map((i) => new THREE.Vector2(v[i * 3] / CW + 0.5, v[i * 3 + 1] / CH + 0.5)); },
|
|
380
|
+
generateSideWallUV() { return [0, 0, 0, 0].map(() => new THREE.Vector2()); },
|
|
381
|
+
};
|
|
382
|
+
const geo = keep(new THREE.ExtrudeGeometry(roundedRect(CW, CH, R - BEVEL), {
|
|
383
|
+
depth: T - 2 * BEVEL, bevelEnabled: true, bevelThickness: BEVEL, bevelSize: BEVEL, bevelSegments: 6,
|
|
384
|
+
curveSegments: 14, UVGenerator: capUV,
|
|
385
|
+
}));
|
|
386
|
+
geo.translate(0, 0, -(T - 2 * BEVEL) / 2);
|
|
387
|
+
{ // split the lid group into back (first half) and front (second half); the sides stay on their own
|
|
388
|
+
const [lids, sides] = geo.groups, half = lids.count / 2;
|
|
389
|
+
geo.clearGroups();
|
|
390
|
+
geo.addGroup(lids.start, half, 0);
|
|
391
|
+
geo.addGroup(lids.start + half, half, 1);
|
|
392
|
+
geo.addGroup(sides.start, sides.count, 2);
|
|
393
|
+
}
|
|
394
|
+
const card = new THREE.Mesh(geo, [backMat, frontMat, edgeMat]);
|
|
395
|
+
// the chip, inlaid on the front
|
|
396
|
+
const chipW = W * 0.167, chipH = H * 0.128;
|
|
397
|
+
const chipTex = texture(chipCanvas(), true);
|
|
398
|
+
const chip = new THREE.Mesh(
|
|
399
|
+
keep(new THREE.ShapeGeometry(roundedRect(chipW, chipH, chipW * 0.12), 6)),
|
|
400
|
+
withFade(new THREE.MeshStandardMaterial({ map: chipTex, emissiveMap: chipTex, metalness: 0.9, roughness: 0.35, emissive: 0xffffff, emissiveIntensity: 0.55 })),
|
|
401
|
+
);
|
|
402
|
+
{ const p = chip.geometry.attributes.position, uv = chip.geometry.attributes.uv;
|
|
403
|
+
for (let i = 0; i < p.count; i++) uv.setXY(i, p.getX(i) / chipW + 0.5, p.getY(i) / chipH + 0.5); }
|
|
404
|
+
chip.position.set(W * 0.05, H * 0.5 - H * 0.166, T / 2 + 0.0008);
|
|
405
|
+
|
|
406
|
+
const flipNode = new THREE.Group(); flipNode.add(card, chip);
|
|
407
|
+
const spinNode = new THREE.Group(); spinNode.add(flipNode); // the entrance's half turn
|
|
408
|
+
const tiltNode = new THREE.Group(); tiltNode.rotation.order = 'YXZ'; tiltNode.add(spinNode);
|
|
409
|
+
const root = new THREE.Group(); root.add(tiltNode);
|
|
410
|
+
scene.add(root);
|
|
411
|
+
|
|
412
|
+
// How far the light slides for a given turn of the face, measured from the rest pose.
|
|
413
|
+
const REST_N = new THREE.Vector3(0, 0, 1).applyEuler(new THREE.Euler(REST_X, REST_Y, 0, 'YXZ'));
|
|
414
|
+
const _n = new THREE.Vector3();
|
|
415
|
+
function slideLight(mat, side) {
|
|
416
|
+
_n.set(0, 0, side).transformDirection(flipNode.matrixWorld);
|
|
417
|
+
mat.userData.shift.set( // tanh eases into the edge of the painted field
|
|
418
|
+
FIELD_PAD * Math.tanh((_n.x - REST_N.x) * SHIFT_GAIN / FIELD_PAD),
|
|
419
|
+
FIELD_PAD * Math.tanh((_n.y - REST_N.y) * SHIFT_GAIN / FIELD_PAD),
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ─── Layout: the card at `size` of the element's height, centred ───────────
|
|
424
|
+
function fit() {
|
|
425
|
+
const w = Math.max(1, host.clientWidth), h = Math.max(1, host.clientHeight);
|
|
426
|
+
renderer.setSize(w, h, false);
|
|
427
|
+
camera.aspect = w / h;
|
|
428
|
+
const tan = Math.tan(THREE.MathUtils.degToRad(camera.fov) / 2);
|
|
429
|
+
const byH = H / o.size / 2 / tan, byW = W / 0.76 / 2 / tan / camera.aspect;
|
|
430
|
+
camera.position.set(0, 0, Math.max(byH, byW));
|
|
431
|
+
camera.lookAt(0, 0, 0);
|
|
432
|
+
camera.updateProjectionMatrix();
|
|
433
|
+
}
|
|
434
|
+
const ro = new ResizeObserver(fit); ro.observe(host);
|
|
435
|
+
fit();
|
|
436
|
+
|
|
437
|
+
// ─── Motion state ─────────────────────────────────────────────────────────
|
|
438
|
+
const flip = new Spring(0, { stiffness: 16, zeta: 1 }); // a heavy turn, no overshoot: a metal card
|
|
439
|
+
const tiltX = new Spring(0, { stiffness: 120, zeta: 0.9 }); // faces the pointer
|
|
440
|
+
const tiltY = new Spring(0, { stiffness: 120, zeta: 0.9 });
|
|
441
|
+
let entrance = null; // { t0 } while the half turn plays
|
|
442
|
+
let ready = false, arriving = false;
|
|
443
|
+
|
|
444
|
+
function play() {
|
|
445
|
+
flip.x = flip.target = 0; flip.v = 0;
|
|
446
|
+
entrance = { t0: null };
|
|
447
|
+
arriving = true;
|
|
448
|
+
if (reduce) { entrance = null; arriving = false; fadeIn(); }
|
|
449
|
+
}
|
|
450
|
+
function fadeIn() { canvas.style.opacity = '0'; requestAnimationFrame(() => { canvas.style.opacity = '1'; }); }
|
|
451
|
+
function turn(dir = 1) {
|
|
452
|
+
const target = (Math.round(flip.target / Math.PI) + dir) * Math.PI;
|
|
453
|
+
if (reduce) { // no travel: fade out, swap faces, fade in
|
|
454
|
+
canvas.style.opacity = '0';
|
|
455
|
+
setTimeout(() => { flip.x = flip.target = target; flip.v = 0; canvas.style.opacity = '1'; changed(); }, 170);
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
flip.target = target;
|
|
459
|
+
changed();
|
|
460
|
+
}
|
|
461
|
+
function show(face) {
|
|
462
|
+
entrance = null; arriving = false;
|
|
463
|
+
flip.x = flip.target = face === 'back' ? Math.PI : 0; flip.v = 0;
|
|
464
|
+
changed();
|
|
465
|
+
}
|
|
466
|
+
const faceNow = () => (Math.round(flip.target / Math.PI) % 2 !== 0 ? 'back' : 'front');
|
|
467
|
+
let lastFace = 'front';
|
|
468
|
+
function changed() {
|
|
469
|
+
const f = faceNow();
|
|
470
|
+
if (f === lastFace) return;
|
|
471
|
+
lastFace = f;
|
|
472
|
+
host.dispatchEvent(new CustomEvent('cardturn', { bubbles: true, detail: { face: f } }));
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// ─── Input ────────────────────────────────────────────────────────────────
|
|
476
|
+
const ray = new THREE.Raycaster(), ndc = new THREE.Vector2();
|
|
477
|
+
function hitCard(e) {
|
|
478
|
+
const r = host.getBoundingClientRect();
|
|
479
|
+
ndc.set((e.clientX - r.left) / r.width * 2 - 1, -((e.clientY - r.top) / r.height) * 2 + 1);
|
|
480
|
+
ray.setFromCamera(ndc, camera);
|
|
481
|
+
return ray.intersectObject(card, false)[0];
|
|
482
|
+
}
|
|
483
|
+
let drag = null;
|
|
484
|
+
const on = (el, type, fn) => { el.addEventListener(type, fn); return () => el.removeEventListener(type, fn); };
|
|
485
|
+
const off = [];
|
|
486
|
+
if (o.interactive) {
|
|
487
|
+
off.push(on(host, 'pointerdown', (e) => {
|
|
488
|
+
if (arriving || !hitCard(e)) return;
|
|
489
|
+
host.setPointerCapture(e.pointerId);
|
|
490
|
+
drag = { x0: e.clientX, x: e.clientX, t: e.timeStamp, vel: 0, moved: false, side: e.clientX };
|
|
491
|
+
}));
|
|
492
|
+
off.push(on(host, 'pointermove', (e) => {
|
|
493
|
+
if (e.pointerType === 'mouse' && !drag) {
|
|
494
|
+
const r = host.getBoundingClientRect();
|
|
495
|
+
tiltY.target = ((e.clientX - r.left) / r.width * 2 - 1) * 0.1; // ±6° and ±4°: the face follows the pointer
|
|
496
|
+
tiltX.target = ((e.clientY - r.top) / r.height * 2 - 1) * 0.07;
|
|
497
|
+
host.style.cursor = hitCard(e) ? 'pointer' : 'default';
|
|
498
|
+
}
|
|
499
|
+
if (!drag || reduce) return;
|
|
500
|
+
if (!drag.moved && Math.abs(e.clientX - drag.x0) < 6) return;
|
|
501
|
+
drag.moved = true;
|
|
502
|
+
const dx = e.clientX - drag.x, k = Math.PI / (host.getBoundingClientRect().width * 0.55);
|
|
503
|
+
const dts = Math.max(8, e.timeStamp - drag.t) / 1000;
|
|
504
|
+
flip.x += dx * k; flip.target = flip.x;
|
|
505
|
+
drag.vel = drag.vel * 0.3 + (dx * k / dts) * 0.7;
|
|
506
|
+
drag.x = e.clientX; drag.t = e.timeStamp;
|
|
507
|
+
}));
|
|
508
|
+
const release = (e) => {
|
|
509
|
+
if (!drag) return;
|
|
510
|
+
if (!drag.moved) { // pressing a side pushes it away
|
|
511
|
+
const r = host.getBoundingClientRect();
|
|
512
|
+
turn(drag.side > r.left + r.width / 2 ? 1 : -1);
|
|
513
|
+
} else {
|
|
514
|
+
const vel = e.timeStamp - drag.t > 90 ? 0 : drag.vel; // a hand that stopped carries no throw
|
|
515
|
+
flip.v = vel;
|
|
516
|
+
flip.target = Math.round((flip.x + vel * 0.16) / Math.PI) * Math.PI;
|
|
517
|
+
changed();
|
|
518
|
+
}
|
|
519
|
+
drag = null;
|
|
520
|
+
};
|
|
521
|
+
off.push(on(host, 'pointerup', release), on(host, 'pointercancel', release));
|
|
522
|
+
off.push(on(host, 'pointerleave', (e) => { if (e.pointerType === 'mouse') { tiltX.target = 0; tiltY.target = 0; } }));
|
|
523
|
+
off.push(on(host, 'keydown', (e) => {
|
|
524
|
+
if (arriving) return;
|
|
525
|
+
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); turn(1); }
|
|
526
|
+
if (e.key === 'ArrowRight') turn(1);
|
|
527
|
+
if (e.key === 'ArrowLeft') turn(-1);
|
|
528
|
+
}));
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// ─── The frame ────────────────────────────────────────────────────────────
|
|
532
|
+
let prev = null;
|
|
533
|
+
function draw(now) {
|
|
534
|
+
const dt = prev === null ? 1 / 60 : Math.min(0.1, (now - prev) / 1000); prev = now;
|
|
535
|
+
let fade = 0, z = 0, spin = 0;
|
|
536
|
+
if (entrance) {
|
|
537
|
+
if (entrance.t0 === null && ready) entrance.t0 = now;
|
|
538
|
+
const t = entrance.t0 === null ? 0 : (now - entrance.t0) / 1000;
|
|
539
|
+
fade = settle(t, ENTRANCE.fade); z = settle(t, ENTRANCE.z); spin = settle(t, ENTRANCE.spin);
|
|
540
|
+
if (t > ENTRANCE_END - 0.05) arriving = false;
|
|
541
|
+
if (t >= ENTRANCE_END) entrance = null;
|
|
542
|
+
}
|
|
543
|
+
if (!drag) flip.step(Math.min(dt, 1 / 30));
|
|
544
|
+
tiltX.step(Math.min(dt, 1 / 30)); tiltY.step(Math.min(dt, 1 / 30));
|
|
545
|
+
const swayY = reduce ? 0 : Math.sin(now / 4300) * 0.06; // 27 s and 19.5 s cycles: it breathes, never spins
|
|
546
|
+
const swayX = reduce ? 0 : Math.sin(now / 3100 + 1.3) * 0.025;
|
|
547
|
+
const bob = reduce ? 0 : Math.sin(now / 2600) * 0.015;
|
|
548
|
+
root.position.set(0, bob, z);
|
|
549
|
+
spinNode.rotation.y = spin;
|
|
550
|
+
flipNode.rotation.y = flip.x;
|
|
551
|
+
tiltNode.rotation.y = REST_Y + tiltY.x + swayY;
|
|
552
|
+
tiltNode.rotation.x = REST_X + tiltX.x + swayX;
|
|
553
|
+
fadeU.value = 1 - fade;
|
|
554
|
+
root.updateMatrixWorld();
|
|
555
|
+
slideLight(frontMat, 1); slideLight(backMat, -1);
|
|
556
|
+
renderer.render(scene, camera);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// The card waits for its print (the logo and the typeface) and for the GPU, so nothing pops in as it arrives.
|
|
560
|
+
const whenReady = Promise.allSettled([logoReady, loadJost()]).then(() => {
|
|
561
|
+
repaint();
|
|
562
|
+
for (const t of [frontDetail, frontSurf, backDetail, backSurf]) renderer.initTexture(t);
|
|
563
|
+
renderer.compile(scene, camera);
|
|
564
|
+
ready = true;
|
|
565
|
+
});
|
|
566
|
+
if (o.entrance) play(); else { fadeU.value = 1; }
|
|
567
|
+
|
|
568
|
+
let raf = 0, visible = true, io = null;
|
|
569
|
+
if (!o.manual) {
|
|
570
|
+
const loop = (now) => {
|
|
571
|
+
if (!host.isConnected) { api.destroy(); return; } // taken off the page: let the GPU go
|
|
572
|
+
raf = requestAnimationFrame(loop);
|
|
573
|
+
if (visible && !document.hidden) draw(now); else prev = null;
|
|
574
|
+
};
|
|
575
|
+
raf = requestAnimationFrame(loop);
|
|
576
|
+
io = new IntersectionObserver(([e]) => { visible = e.isIntersecting; }); io.observe(host);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const api = {
|
|
580
|
+
element: host,
|
|
581
|
+
ready: whenReady,
|
|
582
|
+
play, turn, show,
|
|
583
|
+
get face() { return faceNow(); },
|
|
584
|
+
frame: (ms) => draw(ms), // manual mode: draw the card at this time
|
|
585
|
+
destroy() {
|
|
586
|
+
cancelAnimationFrame(raf); ro.disconnect(); if (io) io.disconnect();
|
|
587
|
+
off.forEach((f) => f()); reduceMq.removeEventListener('change', onReduce);
|
|
588
|
+
disposables.forEach((d) => d.dispose && d.dispose());
|
|
589
|
+
renderer.dispose(); host.remove();
|
|
590
|
+
},
|
|
591
|
+
};
|
|
592
|
+
return api;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// the Visa mark (Wikimedia Commons, public domain), so bundlers don't have to resolve card-visa.png
|
|
596
|
+
export const LOGO = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABQAAAAGgCAMAAADCaTo4AAAAIGNIUk0AAHomAACAhAAA+gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAAMAUExURQAAABEzzBQxzhU3yBQyzRUzzBM1yxU0yxUzzBM2yRY1yhYzzBY3yBozzBw5xhM0yxU0yxU0yxQ0yxQ0yxQ0yxQ0yxQ0yxQ0yxQ0yxQ0yxQ1yxQ0yxQ0yxQ1yhQ0yxQ0yxQ1yhQzzBM0yxY0yxQ0yxI3yBcu0RI2yRU1yhQ1yhM1yhQ0yxQ0yxQ0yxQ0yxQ0yxQ0yxQ0yxQzzBQ2yRgxzhY2yRU0yxQ0yxQ0yxQzzBQ0yxQ0yxU1yhc1ywBVqhEzzBQ0yxQzzBU0yxQ0yxQ0yxU0yxM0yxM0yxM0yxM1yhg4xxQzzBQ0yxQ1yhQ0yxQ0yxQ0yxQ0yxMzzBM5xiBAvxY0yxQzzBQ0yxQ0yxQ0yxQzzBUyzRQ0yxMzzBQ1yxQzzBQ7xA8t0hI0yxU1yxU0yxQ0yxQ0yxQ1yhcu0RI3yBMzzBM1yhMzzBQ0yxQ1yhYyzRUr1RQ0yxIzzBMzzBQ0yxQzzBQ1yg45xhM1ygBAvxUzzBQ0yxYyzRUzzBQzzBUzzBU0yw02yRQ0yxQ0yxQ0yxQzzAAA/xQzyhczzBU0yxQ0ywAr1RQ1yhM0ywAA/xQ0yxQ0yxQ2yQAzzBQ0yxM0yxM0yxQ0yxQ0yxQ0yxU1yhQ0yxQ1yhU1yhE0yxU0yxQ0yxM0yxU1yhQ1yhQ0yxU0yxUzzBQ0yxM0yxQ0yxQ0yxM1yhQ0yxI1yhM0yxU1yhMzzBQ0yxM0yxQzzBQ0yxQ0yxU0yxM1yhc2yRQ1yiQk2xQ1yxQ0yxI3yBQ0yxQ0yxU1yhQ0yxM0yxUzzBQ0yxQ0yxMyzRQ0yxMzzBU1yhQ0yxQzzBU0yxQ0yxQ0yxMzzBQ0yxozzBQ0yxU1yhMzzBQzzBU0yxMzzBQ0yxU1yhQ0yxQ0yxQzzBQ1yhI1yhU1yhU0yxQ0yxQ0yxQ0yxAwzxUzzBQ1yxIyzRQ0yxQzzBM0yxUyzRM1yhQ0yxQ0yxM1yhQ1yhQ0yxU1yhM1yhU0yxU0yxQzzBQyyRQ0yxMyzRM0yxM0yxAxzhQzzBQzzBQ1yhQ0y75UOzoAAAD/dFJOUwAPGiUzPERJVVE6IxcKCU9jeo6it8vc5/L77t3PwrSnloFrUycOFjlXdJGwzer34smycjQVL3vv9dW1cUgiAx5Ofa3++NOshV41IFqNve3816h4Gwg7c+bruxk99N/Mmg0RRYjG+XU/CyqQuGn9pS4M8UY3yItNElIEleNHfGSulBPB8JjaAbMt7NQG6XYCQN4mBZlsko/l22Dgm1YsocNdMIxwVIb6nYnEQ/YdxW8oZ56k8+gxgyF+B2Z/HJy8h4A2bqavQlhQoNbkuc6xX5cUtmF3aLqfv8fY2UtbKxhKo77REG2qOOHQqyRqWcp5ZYo+XJNiMkzAKdKEH6lBgiUE3hkAAAAHdElNRQfqCB8OIym1hFxLAABCMUlEQVR42u1dd2BPZxe+MWImRhSxZ6pBzBJUtH72ilaIoMReTVEztYqKEUITVaNKq9JqbGpVCUpRamtRu8RsFVUter+2X1WHccd7nnfce/7KX/k953nPee697/ueczTNNddcM2deqVKnSevt7Z0u/R+W4bc/M6ZJkyZTZh+XG9dcc005802dMUvWbNn9cjyRM1du/eHmnydvvvwFChYqXKRo2mLFXd5cc801iS3gyRJPBZYsVVq3ZEFlypYLLF+0QkWXSNdcc00i83k6fSW/spV1NhZcpWqhas9UD3F5dc0118S2Gs8+VzOvRyewWrXr1K1X39el2DXXXBPQGjRs1LiJTmyhTZ9/IV0zl2zXXHNNGAtr3qJmuI6zlhF1s7RyaRfAWusyWZs8L+Zsm6Pdc/XSRspNe2AenLU08b7RvgkG0zMCrYVvh46dOvOI5jJ1unTtpoaMhHQv8VSPmp2q5A2nipieRMhf0uW0qKYvV+slbbz07gOkqpwJYK+AMIny9hPSN1vjIJ5x7N+v/4CBcouf16BC/YLJiQqOpkH/qi6xDW6XQc495axIloaYADYU9O4jxCpkHja8shAP89qvpR8hqfq16jLSH8PS0zQOjNTltlGv95UwbPoBGRodYwLYGAymOvyXYGy2flEifdG07dhBuod5dNExoTCGhtH4ME6X3sYPi5UscMYi6almBtkEDKaJfPmPmxQ4WcBADoqo9oZEURw/LC+SnQSaN1hdBSszJUwqAXwTyE1wexPAGoBATeLJft+pb4kbytN6dJDjcR4zfQaWmXwkbnTQ1bDwejHy6J9vHiAzM80gexuDKbQ3N+5n1c0reixXfudZ8Z/nY2fDj4tIdgiq6apYfnnOhOcgeXnXDLL3MJhqcyJ+xNzacgRz7uffF/p+TPy8RDwpGSk8+UAZAdT9P5RlKzA/kJX5ppCBHusfcPlm+6hgLYnCOalmQ2Hbay1YyIORRRSulNUVsrLVpdC/xR4gJw1NneoFY0AtwZOeKltO6eI599JJcSIG8DN8Tk6XUTwVl6skgPqot2UQwPeAjKww9Vb8LgjVx/Bzj+f95YzoGSvFe6hn5cTlaAJfVulqmWe1+Pq3piWQkJWmoK3FgKK61P+w99r0nSQO6Sg/weL3E27J3Z69M5M6K6aAerto0QWwBHJfdJ0paJ+KuC9p07xazJA7ouuIFb6N+DHRgcCd2DRL2pVKVEkBy4l+hWA9kIya5qCBzkiz48iuuCG37AHdRajoTebIRDYqp2I3btr8ojIK+JnY74CZkEcgW0xB6x2KQbUVxXXm99rIH8+tRYreFlwzm9S1Vp9/mD9ICQXcJvSd6HlAJrabY2ISCBZoXz/V1CQFotlfpMswgzw8qdhB7l9Y66FfKHAu3Ehg/YvPBSRiijlsEzGoSkOIDnhTjcf5ToGCtxdncYA00o6uX35XE8mD5ktxBTADkIbcJicz1sHAqgqg2SdbH10N2y1O7O7h3UHic5SnMU+32Cvz7knwWGEFcC+QhldNYiuDgfUJOclx+3Yos6U9XZzYLcebi7rQ21Np9kdIe0lmtKhTqReEAlmYZXIXGASLvB2+dz5dHSsmTOwe4M7FQbTLxQcEjpczbA4JKoDIWwRfmcT2OQaWh7j5cfUCCsmfXlmYE71i/L8J8/AgY92ScsHyxY1nkpD6F1IGyMH7Yp5P5yRleM3QJJX0T/9ClNCN+1oANjj1iw3z7nhYtsA5IuR96KNABt6KNwmuMQbXy5QET6qiq2WFRQldIaanfcPP/2MTR8pVTy5kVXBJgQmIG4XBNZeO3lYFddXsqCCRe1yI8VE9uHLQfutmie4WBGcWT/9SAZ8hiWYnT3YHActIRu83eZTTP88eQUL3ZSHoWMibhjUD/EbJEjv9xRPASkD3C5oFtw8kzFSzzyrW0dWz0YJE7kaPEHQExfOnYo13YGkpYqdNgGj6F4PsyZnWLLrdGFxticg98ZaC+qefFCR0TwnCx7tCsBF74pAMHxvCvQIiJ1CdNo2uLQYYTWXDGT9dSftWjMgdIAofa0XJ5fhnPhC+Xm6UaDMVzgKdX2IWnC+oKdo+CmbPpaipf/pGMa7ACHMbeKZA6Rw7wE/wC4L1xNK/PcCZPJVNDxVrDkLWnWBrYVFnRfUvSIw5X+mFIeS8WCl9pl6ER+DwGSkWW12Ark81jQ40IDU3+zE/xS/oqtpCIQI3ppQwhIQKNy30Yl1xN589xURiKuaI0J5vxiA7xf6A8kVl9c/Cc4zCnhWIkebi3e2IHlBO1CvShUXiqSvQcQtV46AT6o6sab3UWV39w/XOfqSJNDt3riaiiTp09QmRSFoGdDyL+Q1K0F7GIMZP31d1lW2VCIHbXCRGNmuCWppAAU+FowSqBjkDLNJPCTEND1WlzLapb2QBpfVvghCRK9QW6zRNWAtLP0a4T2GBWkMvArr9gnl4oD5d4Uw5zbRdaf3TS4oQuDWEymrqZmr2bEG2aWJF0PPicFMb53WtiubhgTpVMx3sNWmc2vpHNwfSjHUUi5NXNKEtxvusSFOGU4QhJiPQaz8L6wbSkhcYUpolSHH9MznVlMZ8BXvKVNJEt+P7J4tDV0VRWDkEdLqvhdtM8qV0PX/V9S9KhMEO+wQjpaQmvsUdzSHK/eghglDiBewnbqXdwGVQSnsxY7SLR3X902uLELlXBCMlXJPCUgeK0Zn8O0H4QPbTtVJt2wMDrQozQvfr6psIha8bhWNlnRwKqAUki9A164IgbOzEudzESsO9Thhs21h9Y+x2gP7p3zts68aYZdBksbAig7mzlVMMKtIAXbbSBSwedJ5widFRW4IT9E/vxT9wR4g3YmqeJo/FruU9oNrjJQQRwIT1pLaArwIIXBo2dF51hP7lDuEfuBPFoyVCk8nCNoXzpesHEVjoDZylUlXgLUpG3Z3qOkL/CPpGmN9qyCseLeJMSjZmPslc36IXicBBVqDD6awA3IbB1okJm5WcoX8ifOsdFZGXYppkts6P442FQyIw0A+46Wmp315TDDgmUw03OUT/LHS0YG57ReTlmiadcRxXLUIL2V5Af5+yAtArCgOOxVzrhh6nCGAr7oFbPUpEXgLlE0AtrGMoJ7YS1/D3/jrO3SRLteLeIHQX7XOZtpZT9G8G/8DtLyQxnTQZ7dxhTnR9zN11X+AIvQ9E3lVrYn/7unppp+ifAI08fMVsNiHCcGAL1u0dPnTd4O75HKC31u6ZlMSA62mbyoFHHKN/ennugTtdUGZ+1OS0tVz6xFzn7nd+4b8OQGNdbE8oiG/sHP3TM3IP3CuCMrNEUgHUvHk0jZ7N2+vFwF37OZYQLgChu2mXyqUO0r9EX96BW19Uaj6VVQC1YhwmyBHMYTRn7+F8LW3txKcoCF6ATSazOkj/BJhnc0hUak5LK4Baag7Vwan5uhwL3Lavaw0iqLJssk0mxwY7SQC5b92MELbfrHjDgY1bA3yz1Lf5elwCGBgNrEH8SYpjzd5NnaR//K/7ThSXm7TyCqBWrDKaLc4VRetxnlocuBGyXIpjzWWO0j/eHy5a3GRxuSkvsQBqQ0LBbOXg6i7yMr23NYi9pHhuT3GW/jXhXfJ/VGByCsosgFoXMFt8h6tuwDk62mLODJNh5+ZWkLMEcC/vNN0rMDk5pRZArRyYLp41lfG5cH5abXwDalZoqyo7+oqz9E//hHOSilkG/KeJPRz4sdYqD5audBx9HYRzM7i9RYznMfgS7PCYzWH6x32aV3+XHToDl9jwHC+dA+fm6xYh+oDGS2Z1P4BNvOMM5JuivmLPnH9ObgHUsBVNZ/k5WgN44mN1gkRrwfH9/gH8hNNeAI846x3FrJWTXADrQ3u6TePnaGGcl/mtYmyBwdfGxoSLSk7TP92Pc4YKvuWaS3IB1C5APye4DUaKS8F5afm+91kMvp9sbMgHOU4AX+KbnxtF52eB5AJYLBTJVmtebgLvUoVbbpL2IgZgf+s01nSc/rEan2fVtonOzyDZXwHrINnaxMvLXTgfk61iDAABfN8yi1ucp39JfHt+jhD+lXuD7ALYGsnWUk5OHsc1QPRvZhXksyCENSyfgPzsPAHk3MNtovAENZZdADXkuV4+Tj4C9+6tn3T/Ivq29Sbn6Z+d/QIWW9eThSdoVJzsArgPyFbnWOXjaJJllF9gAFq+uLCniQMFMD3X3DwhAUOLZRfAbm2AbL3LxcVXcA5WsVw6HwMqy6lkFWAjB+of50POvRIwNEf6b+CCQLb2qX7Xx/qtiWIghB2s7qMGO1D/wrlmptBlwPfsTekF8KbqbO3BTbDNXdwyynoYhB6rhcpTnfgCOJxrZvaXgaJ+0gtgfEscW/l5OAhs+2WjfTpoaLvV0q5WSU4UwP08E1PwMuA/LShWegUEzlzpw+PMaDvOv2PWUYJqnqyWdvVwov7pXZ1yPGnD0kgvgJ8D2bqId28Szrv11lGuAX2nT3FfAI1baG+eeSlJ58UD0gugDzC4OVwrAI6wsFEXhCr67GsNXnZHvgByHftYXxKSPpBeAJGFYnXhzg3EVRMNjrYO81sMxM7WBhb3HiVUznVuGrGsR6UbA869W716xfa/vabFtW9/vHr1sWkmFf3+u/5LS84+zGbR2/HMym2SCODP8gvgEhxb+AkLi3DODbUB8yQG4hVr6F4SJdsqRxRalC6TgYZeARUyLMp+oa29OXs3OCblCFk67/DdJ2BiDXBsrYA7Vxv3XpLZBszRGIzXRSfxEYlWxW9RGtOHaO3Trg2MyGux7WV3jkk5UZfFfpD/FTAvjq3jYNeexLm22QbM9qDOtPUsofPmnmRvzfzczotG5nSVaprvNtaHY51rXF5pBLCF/AL4Oo6tZ8CuLcW51twGTFS1nrXKzZJ8Myzf0B9ZDOcd2OG7MabqDddzTMkT8hwVPS+/ABbFsfUd1jMvXKWzrWmTqzEYrfXuuB3Kc9cvgelFs+r1EqoY/emVHFNyrzwCmFd+AWyPi/A7WM+Au/ff28E5BoMxwhK4ZH7Jtf59gkKDBfteXmHkxwfwy0gpyoDv2R75FbAfjKzRWMd24t5UfOzgnIABae0W0jROiRX12UaquIjpVW3vY6+/BvBLyKsS6R/Xed+MbCUuqCORflXARUF2Gc7hs1gBx2kwT9TJY7TB4XszYcejAEzml49ylAHfs9XyC2BXHFtp1Tzd8aS2g/NtEMpWVsDxKQMuWx8QHzE/rn54n/+C/PJxukz6p4+RXwBjl8PY+hboVrc+MLdy2AIKajaaYgVbSC4OOTVuUQgqSLonN30whkX88vGKVAI4QX4BBB46IUsHs+KCwN6G+WwMyDpWsHXgkFJn20Oj/90NDxqacI5bNtbX5bIa8gtgCxhZO5U823nR1gtLNKjb8kQr4A7B86lNVnwCbGw0+F8oOodxy8ZtkglgUfkF8GMYWbVwDRR74ULAXufMd0EorUxsimuJTqfTxbikQNzdzf9oSVaWWzJKUwZ8zz6UXwCBYY4bjHQdp+r2bkwcEPgEvi86mxp7ccuCM2vP38cRyA3GRMn0Tz+lwCZgHRhb02F3CfLAfNpmD+kHGJSWGhd9Ak6mC2Fc8yBNwr3zwIbcXkYmyyaAuePkF8BLMLZgT9Y5uAiweWEX1GzF0vnT19hcuh7DOxO85v5fgDLxAnBCl85uyS+Ai2FkjUS5NBvmUil7QCNDMTCXWMAGrJL83QqK8C4RNyBC10tz+/m98glgPfkFUNuBIqsP6Bm/2ANbf5tf9ZNAMD+2gG0rNJG+EGXEWIUL5Xj9tFRlwPfe2xUQQD8YW6Bvi0Ywh8bZ3LYCbXonxVvABr2Rcbi3OPngy+uHr8qnfxxPzNkZbgjf+xB/YkvDHOpoE2pNDMzZVrC9CEwj/76aa3KVAd+7BbFGfuJrwNj6BeIP7tstapUcuw+NLEALQKbRC678STMN+F9WQQHmYU2PqkLcWQ9bfLu14KlAOLdawPYsMIlKxbnyJ1sZ8D17SQHmE1BkhSu2lTzEJtQMIJxWXlTnAZPI21U/br3H7NpSBagvAWMrM8CbDTBvcsbJAdXSvY7GwBNgV/1+s3fkFMDxClAfALs2AhiMFI/r4VReko91KzsPcbh+YthGkaKadGXAf5oCw4E17WeFNrsHwZY+aaBNqCiR+cQCtlm4FDoS48qfhGXA96yrAuRPRZEFmKOHu03/qV2o3UFArUxuqIfLoO9c9ZNqGvC/7CkF2B8Ae9iTu1IDdwTysV2soPbnnj0WsNXFZdAqV/407ais+oee9khikf4gsugHIxWGLfx821h3Y4BOs4IN1yPoLVf9NCnLgP+0F1WgH9ZAuTmxIyEpsIW/bBtsKQzQl61gq+2+QSAtU5S0Ashzhigzg136mqLMl8QK29X7vokYpHOtbEnhziS7uPJHWwZMXWF3QgH+vaU5OHiM7YLl7UrbWJuDkD5pAdsq3AvEHFf+SMuA/ecKeMlANAtDPfBL0fpxPBGVtqG3bYOthkGaaKW7yRCcAJ5w9Y+0DHgv9cOsgAorcAoU7bXiSd2oBEvb4fbBbhb4mfMtTgCbu/pHWgb8jUbcHUmF4cDac6hwf5rSi5icsLTdYh8t6OZXIdH2pP5ld139oywDbtNbK0C8grcVWIKMqHD/ktKLV2BZ29R+9cIeUAHiPivggN1Qn3UFkJJtP/q3m/QKLEE0qvSzB6UXF2BZy+A0G3Vg3d0KuJ66TFRKbqRlwEPo3wuuqrAIVUHh/hOhDwGdUUm7vLh9tMkYqNYGF+7ECeCbjhfALoTs5grRtPbE3xpfqbAILUDhTjkYCVdP/ioDtDkwUBtbAheOE8Cvna5/pNOAs//+C4eJn7EhCqzCu6h4r053BHIElrSzGKAFTYCwNLYkJhEngP6RDhdA0r2QNL//AvXYs+4qPIZaguK9KJkLk2A5y+Kd/yII6yBLBzQ60LY6XAApy4Cb/vELRYhXcLoKy4A6QFhJ5sEyWMqyOPZqCMK6zgq4W0gBLOds/SMd4VDpj5+oIMGOEHd7CRTuZPfGB8LKV8NZ3OYOBGG1BK4CUgBDqztaAPsTUuv5f6ux+CTaFbyiwjoUA4U7WfejRbCMZVL82AmD9TNL4JojBZD2apToRjoNOD8m2FQYDqxpg0HhTjUYaTwqXxObMUBL/VC+Z9baLW+BCmDQbQcLIOk04CV//kgP4hWsr8JKoKZSDaGBD6tlYdPALg0I7EeW0KWDCqC+zMECSFkGXOve1JrLxAt4SYWVADVo1/fTwD8ES9fWLOAWwWD1nLGELgNWAC1NLVHDSKcB/9Wxozrx+m1TYSkWgIKdZjCSVzAqWX9mghdUbLvdGrqtYAEsc8apAkgaB/evnBE3hKmixFqMxgT7YRLwl2DJuoQJ3qYYsIfE/hr4y8Y4dDQmaRnwqPudIIkbwkQVV2ExXsfEelRvCvCw4tVRTOB7gWZAWNycWYsWQH2oMwWQsgxYn3n/d1YTL98kFRbjbVCsZyTAjru4xubOBmoGQQVr8IbBBdAzzIn6R1oG/PeR5dQdvpUY7DIC9FZShAB7AixRizHBC2pdHWRxclM9uADqoe87UABvUjKa8rc+QGeIG8KcVWI5TmNCfSZ75N1Q7Qz1g2wAg4Y3dbII7zJeAPXOHZwngKTTgOf9/ZeIG4XMUGI5smMivS175LhPtixsAIPaTVn9Xi/KQQD1No4bj0Q7DfgfLYtOEi9eZhXW4yYm0AkGI8EGu6ewaX2GunL0jUV8J3gIoJ5Yz2ECSDp5Zec/fmoK8dodVWE9IkFd4HqxBj4WlqPfsQH8Pghuaov4nuQigLon2VH6R1oGrLf4x2/VJ146NVbua0ycM3/QX0dlaK0ACR79962J1dt1xXRO9lq8gwSQ9LZlaKp//FYsca+kqkqsSF1MlE9l/STNg8rPk4wQj8TAzWEVX0VeAqhfcVBjhCeQp3XELzctlVgRUBMQ1kNU5sDS80k2gEOWY+D+ahVgvIebAk7Y4hT9o/0q/ff8Wer+k6tUWJIwTE/RyozrnvKjkrOUZHuWNy0jXMFNAPXQbHHOEEDSMuDgyH/9GnUH8reVWJNTmBhn+7QoBntducEIMerWjvW7CVd0jrbwohP0j7QMWN/875/LRLxo/ZVYlKGYCB/EFHQjVGLm8WGEuB0G72TrCGvyFEA991oH9EagHeL6n2spMcRTz35SYlFA1x9+ZYk5tjQqL5k95EAlNzYaj03V+VrPZqrrH20ZcOn/nqZXpV2x5SoMB9aiMSVlY1hiLoFKyqhMjBB388cAfso6xCmcBVAft0/xl0DSacD69f/+4CfEKzZLiWUpAInuHSwhN0blJLOrTj+AANtoXb1F5275jyktgKRlwHrf//7gM8TrtU+JZSkv+u78f6x6FCojmZWqPgU6Tu1mHeJx/gKoJwZ2U1f/aMuApz3g9Xkg8VlhISXW5WlMbL/CDvEGVD7mZXY74w4G8Gk7GMcJoID6NHVnhdDWAj1wcCtxx/cn1NiabQmJ7InMAMfnQmUju56PMzCAbbUd+0oXwsakVlP/aMuAPQ+8R0TcEKazEsOBtbOQuC7IDO8gVComjWAFOQCE2FaP5Y5iCOBv38FeKgog6TTgh7SB/JZ4qTYqsTIHIGHdVJK95L/ZIWaQB4AQ95LiwfJYy7VPwcoQ2ovmD+65vpF4oaYosTJvQIKa2WCkBaGoPKzAjOJfMIDbRNsB2UoXx6qkV03/aLUocc8DfzS2Fu0yvaPG2pSBxDSjrgJaYVQS9mPH8BcYxPml2Kg0ZhEfqyWAtFOhH3bNlrht8HY11uYdSERfYgM2JAWVgteYERwDat511R7MD0QSQN1ztrpC+kdbBqyXeMjPvkm8RmpMt/8SEtDt2IC9iUrAlmHMCF4Mgmzzu7GhLpZ1TqiojADSlgH3eVjJ+jfES3RXicVpBglnRpeGdqHSbx47glEjJ212Fg3wCKaAep79vmroH20ZsL70Yb97kXiB9quxPEcQwRwUzQJqKlBRrR7KsE8xqH//Crs4S+nCWcocJQ6EacuA9Y8euvlCfLu9jhoCuBsSy2NZQK2EyrxdDPkti4Fsu+HEUF1Ay/eMAhlG25dlx8MfEsR3xlLUEMD0kEiew+I4IScq7xjO7F5TCwN5qF2gx3QhbWFr2ROMtgz4UYdfycRr00qNIypId4HsDJC+gkq6aQy/vM6BMNsvtz4spgLqEb3kTrD+tPQ8/fBfTke8MjeVEEAtHyKKTzEAehaVcnMZsvstBrJnoG2kKwUVQD10aQ2J04u2DFiv/ah3G+KDrcJqCCCkxTyDwUh7QB+TenB7huwuw2A+Yh/pRY+oCqh3TsgsbXrRlgE/+ih2Gu1v51BDADGX6xrYxjkRlW4JLNkdjcHsxwDqfF1cW96xuKTpRToNWI9awPHp20SNLt6RkMslGWwfgRxBJdvTDMkdCHqt2sQA6zBdZFsxJVbG7KKdBvyYmdubiNdEkXKdToj4tT0YaRIq02az5HYICPQ5Blh7jxJaAfXJlyV846AtA9azPvLH+xKvSAk1BBDSrqScXZQvo/KM6aquBm2SMelO2UMX3J6YJN0dC9oy4KBH1+NS38F6Tw0BhEzEGSx2JN23XEy/tDBTp/QrTMBejBJdAfWIsXLlFvHG9dnH/DzxLfyRaghgWBIidPfYA7kIlWJMhxhrEzCgX5VKru2Y/26ZDoTj8tKykeUxv0/cEKZNiBoKCJk0abO6YjwqwdaxZHYVCHU9NnCf1CWwNh0j3RsWfx7DPm7n4xrxWoxVQwAhNbb2hgxlRKXXBabMbgWhXswI71cyKKD+1lpZ3jyIq3ELPe73UxOvxA01BBBSrrXZFsRDqORiu82eHQN6FKvaPW9dDtsuR9d84jJgvfnjAFA3hNmthgBG90HErB2EXsGgzKrC9qbFbAzq9cwAj5REAfWDT0uQWLTTgPXJjw/WHLQI2qohgNoYQMTaGox0CZVXl9g+WUC6/QszxBs9sihglF+A6GlFXAZs5LyOeIhOoiI9a6shItbOXd2doKzKzbbg6mMQ7CzsINfRpbE8i6LFTiviMmAjO78nBE5qgawXIl5fso6vAiqnXmPL6xIQ7FTsIL/RWR4F1E+LfTGadhqwoTkTe4jf6L9VQwBjSgOi1UaPgQRQQnm6s+UVNGptMEvMH+oy2Z0F4mYVcRmwvsgICOImwn5qCKB2BxCr1qsVevcBpVNjxrSCLi8yvbrjM0MqBQxOFrZHAvHEWf/jRkAQV5A2VUQAEd9qSZZ3bLKisqkoW1YjQzGw2Y7nSq/LZfn6iplT1MWbxrrxEVdQRXmpIYCpEZE6yyo60Fwh/a14tqzeBeFmvBM2XDIF9PiNEDGnqPtXfmMIBfUd3y2KvAKmAAJ1jshHNL/bc5KlwL2ncCRb2KnySKaAeq7L4mUUdRnw8m6GYIQRH2q9oIgAbgOEaSOL2ECTdfXEVIxJBb1K1WYdDNN16SyHcIch1I3WjR4/EB9F11REAOeIe8Tgi3ohKcia1MEY3EuZR0NJ+RSw8j7BMqoqscNDxHh9KKOIAKZCxKi1MrN6qBxqzpjTViDcS5hHQ/sU+RRQ7ynU7DjqMuAJ0YK82rRSRAERU2FvW0I2G5RA51lTmgEE/GP20dA1VEIF7LNWoJb5xNOAjQ/afoMYyABFBLAQIEI/twLsFip/1rKmdB4Gd1I8QTgk6zJaz+OipBN1GbCexiiSGOIdpJWKCOD7gPhMtgKsESh5KndjTekpDPD5FOEQc1ZKBSwtyvsIdRlwU+NQehI/dBQRwBGAeRC7LOCKLQ3KneysGY0D1a80IomH3rWlVEBPgo8Q6fQEsZ+VjEMhbgijyHBgTctHH51WToxQPZU9b7Am9BgI+VaaeGjQUkoF1Kv0EiCZNlJH6yrjWI4SY8mkiAC+BwhOCzf214Py5gvmhKJu01EFYIdQORUw6SX+yUR9rdbM7OqKcj6A4XYUEJveplFVR01qZL95tBsDfBzZJ0gLXVIrWZxzLpHPcF1iBg1xSUojRQQwMpE+Mp8yjWoDKGdmsB+zUwqDfC9dSOyWVQFrp+abS12I/es80AyazeK8jQptC+kD82WzmKLDQSmznzmdvokY5J/QRUTcZlkVMPcgnpkUN5nYveGm4MylBRMcrYgArgRsUJvFNAiUMLXYz5doDoKejjAk4sfIqoCejhwnZ1KXAZts20Y97bmXIgI4iT4sQ83eUdgLyheCxrZzQdD3UMaE70hZFVDP0Z5bJlGXAVcOMwWHuiFMVkUEcE0SfVSa7F25AHUQuZE9nQUxyHPSBkXxJ6RVwGm8NgKpy4D1diYBEa/h66psAkbQB+UBc4gKg3LlCQI282KgFyQOiorbpVXAFRv55BF1GbDe1SSg12jh7FRFALPRx6S5h0UIqinJdALhAEGfSx0VAfmkVcCgt3mkEXkZcEqcSUTE/ZRUGQ6s9aUPybJibSb/aXkIyqeOgrBnJA+LM/OlVcBQHlMbqcuA9XlmERUjBvSkIgIYUpk8Is0NRtoFypMPCcgEfb0jnr4+PaVVQD0wDp5G5LumpmfrUDeE2aTKN3A5+oA8ZgJOKn9MkkRVJ+ASdH4N2X9ZM1xeBTyJvg5DXQas5zOP6QtijlURQMDNjW9MwBkKypExBFTGjMNg3435NlgqrwLeiccm0TvUDrUwj+lXWkRHVBHAsfTh+J5xNOT36e8ZxVXiiyDsNzCREbdBXgWsA1VA8jLgUAuju4g30z1nFBHAmBXk0WhiMNIroATJSbFN9A0I/DFUbGwNklYBq4YBc4h8FOpBAa8keCsigNrz9AeuxnuXXADlR3kKJgMx2HPjdrgyTpBWAXPg7mnQf7Z8aQUWMarvVBHAtfTBaHh0V0BnTHYkDaRgshMG/FfA4Mgk75XoL9agSCK//RQcaQVWQVpQw1URQMDOVRZhviX+tE8piIxPwoDfgIyOyALSKmBJ1Jsy+eH/ZkuwqtGCGqyKAGr0pRfJRvcjj4By40cKHiuAwBeFRkf0hx5ZFTAQwxB9/96j1vYviFGpMhxYO0T/LDaIZBIoMxaS8FgEhH4dOD5u5pFVAV+A8ENeBlza2pE2dUOYLKoI4BzyQEwxiORlUGI0JOHxHQz4cHiA1OgnqQB69gHYIS8D1t+0iKwtLaxfVBHAVPSRuMcQkBGgWxelabbHm2LQf4aPkDWBkipgYjp6csjLgM32k/vLXiU+ZVJmE5A+cz8yhGMRKC3qkrDoBRrllI1HiFxbLqcCtvmYnJor1D6MtorsS1pcygwHpn5S6EYrecZjkiL0NgmLHUA5/RGXGFk1W04FnLGHmBjyMmDrA2AWEwO7qIoAFiVfw2UinFrdM6ILTJVA+1qcSpCi9ydKqYAHiS/D0O/8vmEVGnVDmIaqCOBA8o83Q4ORDoFSgugVCtTHazu3MNl4REoFfI6UFPp9607WwR2kRTZVmU3AneRfnQa6j3oFYxJiNNHWBWiY5zZ+YeIbKOOVwCjSgxD6q/tFrIMjnvk4XxkBJL/JZGQA0UughCDq5LgABL8Iz0C5uUNCBRx3m46QOPIpMIk2NjEH0EJTZjiwlo48CJcI8Br6f1vuRcPh+6B0rs81UrzaSfgSeCWWjA/6IQh2GlcGEGN7WhUB7E2+v/34Hp6oQjKqbqL9MfBrreEcK60Py6eA88jYoO8BXsIOvBdpsQ1T5huYfAROv8dCSAAlA9VIe9A08a+5x4pvx1DptgF/IOKCfBqw3sfW7C7iVncJygjgr9TrGPy4ywi9c2NyYSQRgyGgm8JvChAtG0/LpoDTusn62n/IFr7ytODyKSOAk8gXsvtjEAwDpUJ6IgZ7gfB/I0K4RC/qI5kCZqd5GaYfAmPvzlZzWnD+3VQRwNhg3olbFpMI4VSzIr4HZXJqMQKmlZ9kH8HnKFigLwN+y941bl/izf2MqgigFkG9kv0f/ftjQYnwCRWBMzH4xam/vCtXr+jTFE8+8mnA+lWbCEvRwlukjAC+QL2S6x/9+9cxaZCYiorA8xgHBOrAEbY6SSYFnEuwF0qP2u5Fk0K08JYpI4Dkaznu0a/qoJ6bd6j48wHNc/9VpKBp5ifRpcDKe5j7T18GXMUuxOm0+EYrI4AhlanXcsGjfr4eKAuorkNoP4AcuClW2HQ9L48CFmLt/B769pX77WK8RYvPM0IZBSQv5X/2Ub+eH5MDtcnoawHK4syiPTgPtJRFAP1nMfa9C/3RzQK7GONG0SLsoIwAbuJ5/HALlAMvkdF3FuPADPECp32gLG2yyrF1nL4MmMUAVOLTzWzKCCD5MeyjOrlnx2RA7uJk9KVgPLgjYuikPivJViDbqzA36QFntY/yF35ZLZfF5OL38hJbGpMAdGMSj4NSuIWYwbMxvxQCeJCp0/RlwEEMet8+SwsxXBkBpJ4jr+sDH/rTWzHx77lFRt4AUAr/IOrjM/00GRSQJX3004D1CwxgZibG2EwZAfyeejm3PPSnG2PCP4KOvF8wHoSKW3oUv3aC+AJYlaHD79HDZTJ4dwYtxs+VEcBM1MtZnuOzlHqtIjAenBc5gHonBwmvgO8y8xZQBtyESeezO7Qg6yojgNS9w/STD/vhjpjgL0PXvzamMsaFdmJH0LqEKMEF0I+Zr/RlwIwuLhJf0DqojgB+QLyeD7uEF58LE/yV6KgrBsrf70WPoTSnxBbAxFasPL1CD7Y5E6BpaUHmUWY4sHaNegPrIa0dB2FivzPhHWJUIUsv8aPIO5/QCjiUkZt96aHOYCMt1A1hUisjgK2ob3M9ZJpFDkzoU9Ztv4ZxQYohNHHpJwssgGUYTQmmLwNmVvZNPGvnG2UEUKPub7T2gb+6IBQT+k8SMvcExoWRcsRR7NrS4irgACYuAsqA9cWMlmM3Lcwe6ggg9XvMgzd1f8UEPmX37jW1MD70lyWSIpOF7ZRVk4mD++mBXpHkuGahOgJIvRn3wHk+IaAisqyExJ0DJW96eWKpWUKomALYeSCLz3zAVz6zXqPdaXEGxSsjgAOJLzE8cDDSTUzcV/YhJG4TKHlvyxRNt86KqYAsnoSAoPU/zmohqO9ovauMAGptiRf1QaVouzBh34iSt5MYHyZIFk4/fC2iADZm4FlVepg52K3Deh5b+1LaVeJFbfiAo2dMJ2XPG5S8gepgy0kXUANyiieAUfavQ2UC3PdmeLhalxbpTHUEMB3xol79708OxUT9XtKtA1A3qKHyRVT8olHCKeB0214ByoDb9Ga3CFlooZ5XRwC7daal6r/dCGJArwhHKWl7BpS5r8gYUyMC/QUTQNvnwIAyYIY1e+S92kK7qaOAs2mp+u9gpFcwMT8jhJK11RgnZJ2/UEyw05A2YTYdApQB60NYrgDxRYvm6ghgYeJlXffvH7yAifkupKwVwDhxRNqw8j4tlAJusekO4Nr7BKY1P8RPoGrqCGBX4nX99zX8PZgbxLUqkrIGaoR3Ut64iquXSyABXGnPGUAZsJ6dKf3EDWE2qyOAa4JpqVr9r9/rgon4baSkrQLl7SaZI6v4BnGaBc6358o2AMQ0TMlvTQt2mjoCqB2kpWr4v45AjmAivj4pZ6CG/npfuUMrU1VRBDDR1q49ogy4KVvqfWjPoRQaDkxd4/jiP39tEibgy9Jy1gjjRecw2YOraBlBFPAjO14gvlpYt64kblH2ijoCWJ/4IPOfhZgvY+J9Di1n8zFeXJE/unySawkhgPvtbGcCyoA9qxgT/7pces3RQojrBu/+/cdGYLaFWtK+OkUHY7L2VRXi61gnEQSwjg0PjgLwzWZN+w1avCUV2gQsCTwxn4sJ9w20jP0Iytp6SsRX3Nrl/AUwxYYDewH4ljB/7tDizaWQAH5LS9U/LriPh0R7aANaxpaAsvaWIhGW6Sv+Cmj9WhRigiGTjl3/fOz0oUW8Th0BJH5W/H0wUkZMsFO3EPgA48aoOFVCLOYA95dA6wPS+wPQfcaec+LJ2xnUEcAY2hur/n/bj9uGCXbqIyrMeyzlVHe4NeC9E2i5gxOiDFgvyp7xebSI5ykUnJtpqarw1w95YQ4PphG/OUWGYnJWoQHUmhadzHeAsOXukIgy4FG+7An/3H06G7VhtFTdn2x7CRPrc4n52gLK2SyaUvbRWzwFsKpV2IjpV+0I6G7l7s8Y3uWlparQXz+EmR8b3J6Yr/2gnE2llgBqASM5CmBTi6A3IsB1paCbuCFMMYUi80VSpjrd+5n6mFBvR03XcIwfZTTVLPpNfgLYxiJmwDRgPYXkbYq47dI1hQKT9lTzr8FI7TCh/iM1XTswftTR1LO1idwU0Np3AaIMmOhA4Sla0IEKhWVDWqr+HPfcOzck0OdTs5UKlLH7FRRA7e44XgI41hJeSPOiNnkorA0t6E4KRWUr2gkXlyFnLfesITVbn4My9q6KAqjN4tUmMJ0VtIgyYEktKV6hqKxCStWH//+RspB1mbCGmqwNmACLKq6kAGqrOGmKpZlrN12he6h9rFBQXidl6uAfv9ELsyy/kpN1CuNIbU1Ruz2NS75OsYJ1r6tzD7UlCsVkBlKmSgNE9p75k9coUhdZ3rOlqgqg1iwvj3x9zgLS6lGuzj3UPlUoJM+EklLV7Lef8M2jyNHpMVB8HVBWALXFeTjkq5VSkPdcmXu4nVYpJGmvu9/87RfqYVZlCzlV00Hx9aO6Aqht4XAbZqZ5mL7jXJl7uKk0HFj7kPzrYzZkUbbHkFO1GxNeQfEKCyCkwPZf9o4UKGWytAoF5BBSpmpq2i2Bd7rNWSmMJws1pa0HPF0LivZdJL2VVygefUgnN0zWtOyQJVlOf3XEF/T1ll1tAVyTD52u5neH+7oax/qRIq7lp2TKc2ZNS8iSAIZogHq63rs9rqyNRW8DjjENcZurcY+0nCrFYzIpVZNAg3SP0RMFmmqiZ1JcALWr4HQtYBbgniBX4x79XqPQcGDiSfJzG0NW5CsAUQUxwTUuRnUB7A2uiatpFuBEV+IeY0MUCsdY0tLp+R7IgrwPIAp0iXevprwtwmbrZpPw3DLgx9pzKoVjT/nXY0c0PU17QL4kqy+AYSug0WG2suaoK3CPs3IqheN+94EkUlqkU18AtV+h0bHbJDq3DPixptJwYK2C9MuR2Er6w6L7VtEBArgAWmnbwxw4twzYgC1QKBpD8si+GpBrSTkwvuTUnGDQUZkdzWFzy4AN2CCVonG47KvRGkBSzDiFxJy7Qc9ZC5uC5pYBG7ENKkXjFMkXA9Kc4iLImWqOEEBoqYW5CQPTXXUzYI1Visbuki/GEgRJDUHONHeEAMYnAePjhilobhmwEVNpOLAWEy71WlSGNOcJBJ3n+DpCALWfgQFyU9h3U4ltsUrR+LLUSzEVwlE/jDOlnKF/2mfAANloBtg7rrYZsjkqRWNWmVfCAxlUH1sL481uhwjgbmCENDCByy0DNmhvqhSNt2VeiS8gFNUXcr9KXusPjBAfE7j2u9JmzPopFY55JV6JLBCGLoG8meUQAZyHC5BgE7DcMmCjFhSrUjh+Ku9CpIRAGAJtDeUOcYgAvo6LkMkmYLnTgA1bGpXC8bK86/AdhqHtGG++coj+aWdxEWJmxEBVV9iMmlKzCzN7ZF2GWgEQgrxA9aEdnSKAjXEhYmImUia3DNiwfaBUPI6XdRlOYvjxBrlT1CkCeAQXIkPFPJqR3X5WKh7flHUZ+mL4qQRyZ51D9O8M8JOjhGFUbhmwCYuKVCkgP5d0FXaC+NmFcSfcKS+AzwBjxPiceXcasBn7Qakncqici7APxA+oWPAzpwhgMjBGvAyjcsuAzVgLpSLyipRr0ARUOVsD5E82pwjgQlyMrDAMaqMrambseaUisqOUa3AVxE56kD/eDtG/1MAtwNmGUbllwKYsr1IhOUTGJfCkBrEDOh30nHGIAK4EBsmrRkG5ZcAmTanhDT6dJVyBqih2fsL409Qh+hc3Axgkhqur3WnAJk2t8V0j3RV4qIUsx/izzSECeA0ZJGONqrJbBmzSVisVlJ9IuAmB6krbC+RQEftQYyQItXhk741goyOj3WnAZm2MUgLYWr4FgJ3Dfw9yqL59qBP9MgsfakWQQWL4DMSdBmzWJiglgLFtZOM/aSCKm3YYh2ox6DBUUx+1OlLsSDuTCxklRocCu9OAzVsNpRSwp2z046qxT2Mc+poB1B2//Z9x+31EDrTN0ChpaBCVOw3YvKlVud5FNvphDcm6+WMcYtBlvNn//1N4EXHbVV7DRonBm1JuGbAF+1ApAUwjGftfw5j5AeTRNftQM9z7X2WqCfoh3GAUNEpmGITlTgO2YKeUEsC4JnKx/41y78YMrnV/eP+/9QlsJmCUFc+HjZKZBnG5ZcAWLHecUgo4XCryS4fBiLmA8agygyssX/39H3b26y7cUdtBcJi8bQyXOw3Ykt1SSgCnSMV9XRwxKRiPejJ4i+/zz38ZNaa5UCEW4weOkiiD5VpuGbAlq6eUAN6SifrQ2zBeMoNcWmkf6qz//tf5l8U5DwmZiQ6TtsaAuWXA1uy6UgL4xxUKWQzYOS8LyKUB9qEOe9D/ndBxlRjxFXYBHibzjCFzpwFbs7JqCeAyiajvgKPlF5BLx+1DTXjI6/KuEwKM2xzRCR8md43tHLhlwNasllLDgbUb8jB/GFjzCtq3n8EA6vmH/ve3Oi7mHF1jD3M4KTNWCOyWAVu1CkoJYA15iN8E3LivjHHpjn2oPo+8sd120QiOwVUvmEOY7DaGzS0DtmovqfUNnFMW3tt44UgpBvLpKftQH9fQIunOoDA+kTVwOJc4+cgQOHcasGVbqpYAtpOF9wQgKXNAPjEYsvXU43+lj99NDvs2RQdzCZMJxnY+3WnAlm28WgK4VRbeewFJeQ3jUmhv+1DPGvqlJp+mw74Hdo/gFCaFDMFzy4D5Rq1AltkjB+35kaSA5uX9zACq4Rvby2t+uQfFn1ejRF5xYuwM2C0DtmFd1XoFrC0H61uBlKwBDUuZaR/qcVPP7tkTEefCe1ZW5hYm4ca+gN0yYL471yJZoBSkh8cDKTkHcup7+1A/N03k2bXrSMkLSO7DMU6MVUu6ZcB27I5aAphFCtILIyn5FuTU0/ah1rXws57T2Y8S7ePEdH2nFs8wiWpgCOY2V8Vs2GS1BPBMqASc+0PbPIHKYwzP7nmErbf424kLGxVlzum6StM4x4mxoaluGbA9q6iWApaVgPILUEZGY5yabR9pnK1Wo2Wen3uO2QWZGpe+4P8oNVZb7U4Dtmcn1BLADRJQPglJyEDQwXgjBrdNbIMI6pcwZZLdUVNxfX85L0KYpBg6AnHLgG3aJ2oJYAfxGcfevRwC8qqEfaisrnMMzvHhtXd9LUGI/GhoAVFu1T1nCLFbBmzTCqglgD61hGf8EpSQ50BeNbAPtRBTQLk6LVt5Y9Jto11k4hoM2ZRwWqAt5FqtDOGu6kqYPVNrOLCm/SQ64X0ioXyMkSeMSlEAS5w2+7OZdavNSVehxgNnbe7p3vXypnl3zieJFievG+LMLQO2bbfVEsDVovMNbkI7QZoPCV/ygovOeVKmtZ0dERHxxdldEY3bth2dNzxR1DDxr26INLcM2LalV0sA04rO9zEoHdWF2rCSe+Wg5mfsoeGWAdu2q2oJYHRuselej6UD1R5iiH2o5d1cvG+eWYY42+cyZdu+UmwTMIfYdGfAspEdlK8MWpXecXPxvtUxxplbBmzfcoeoJYBdhGb7rXgsG/Mxbo1mAPVFNxfvm7FW7W4ZMAvrrpYA/ig02ZWwZMSD+riftA81wM3E+3bWGGfuNGAWNl0tAYxrKTDXnTOr+TRgMOJkgJuJ98PkoiHK3DJgJvaqYpuANQXm+mUwFwdAfp2zD3Wlm4l/WX9jlLnTgJnYFcUEsIjAXGcEc7EU41air32oB91MvGd5jJUzu2XAjN6316glgLfEpfo8movx0jxDY/K4mXjPvjVG2U2XKTZWX7FXwB3CMj0MzERkKMav1+xDXezm4T07bPCmgDsNmJFdUkwAT4pKdGUfMBN3QY4xOEeb4+bhPUtnjDG3DJiVbVNMAIW9Ht8IzcQLIMdu2Yf6qpuHf5rRg7L3XKoYWRXFBLCGoDx73kAzMRzj2Kg4+1DdmoY/bVyAMcLcMmBmFlVcMQXMKSbPOVTdDY2wjzSss5uH/7drsn/nSGiTFBPAdmLSfBPNQyqQY3XtQ33SzUKTT0n3lZmddVFMAEsIyfKL8KLrDCDPGDR4WORm4R+23Gh7zo2wLckE/kZ9R/SsYgIY4BExtvfDedgA8ozBSMqXXe37ww4YJQw1DXhyjAAJPYvYyRmKCaD2s4ChXSsATsMpjGeDGUDN62rf71bTKF+wMuC6IuRzHHWTz8yKCWAPAWMbf9korg/Gszr2oY7wuOL3+6PEcFtFWBnwWCESmnrSz1HFBPBZAYMbX29zTJ5v+xOu+P1m/s0NP9tQZcDbxUjoq8RuJismgF6hwgV3KTwLN0Cu3bUPtbCrfr/ZUMN8wcqAK4mR0OmJ3ayq2iZgWeGC+0s8Ca9jPGNxjTSHq366/lOIeHylFiOfbxO72VI1AZwnWnCPC8OTUArjGoNCohi3qkHXyxw3zBesDPgJURJ6BbGjqxQTwA6iRXdHPAe+oJm3H9iHmtqVPz3JxCYxrAy4migJXY7Y0bcVE8AwwZqFR3F4wjQH+XbAPtRrrv55tpp4tKFemKOaiZLQzxF72l8xAdS+Eiu8y3GgADVn90f7UN90BdDMOSTqdEv/SZh8HuIYTyV5Ypi0IRwoeB7jWhCDQZ9lHa9/w8001IGVAR8QJp8HEl8UXa7YcGAto1DhPS2OAwWg4opO9pHG1nK6/pXqZoIuWBmwf0VxEnoasa/HFBPA6NwixTePveSKIN+y24e60en6N+24Gbpg04D3CpTQ1MXi+1T7BhZpYELSQA4EoO7KXrYP9VuH69+OBmbYwk0DridQPs8l9rWQagL4lEABPpMHAajiikz2oZ50tv61NDdRAFYGHOTloD2tJ1QTwI8FivA0PAjoifGtCYN2SaMdrX99KpgiCzcNuI5I+RxGfKtVteHAWpw4xQULefiPKq5g0Oe/vaNbwQSZ7Md+FIYsvVAJTV3XtFG1V8A6woT4ZR7uo4orku1DfcbJ+hfcwSRbsDLg3L5C5fNuYnenqCaAl0QJ8dJcXq5RxRUn7ENd7WD9W262lQ5uGrCfWPlMffv7HdUEcLEoMb6Si/uBIO8Y9Lmu6lz9q3zOLFm4acAnxMrnscTubldNALXBYsR46G0u3vfDeJeXAdQJjtW/Jqa75OKmAeeJFWxTn/hmr+eMagLoJ0aQD+fifCzostjz9qFWd6z+hXc3TdZ0GLh2ouUzdVv8u6oJ4HQxonwLF+frg7xjUONy2an6V6WBebJw04CFGxZO3RZ/v2oCuECIKN/OZ6zgFJB7ze1DnepQ/Vtv4ZurL+7tVLjuANRt8euoJoDk9dOGjNPpOqhe1N/HPtROztS/pfHiLutvNlW4dKZui5+inAAmCBDmyzmVEx3GuLfTPtL4JCfKnyfZCle4MmC9r3j5TN0Wv5VqAvi2AIFeiI/rXqDbYgn2oaZxov4lbbXE1UQYwMkx4uXzGGKfb6omgAEClFhxmiuNmolywz7USw7UvxRrQ6JxZcB6XQHzmbrJcWHlvoFPc4/0rzh5PhTk3yxpditFsgLtrVF1EwdxrIDpTF0ymUM5AeR/vvg+J893gXY4GRwVNnWa/IUmW+0PjpueLGRZBHVb/CYxqgngAN6xHh7PyfNwaV5wvaIcpn8tX7FKFa4MWK/kyGsd1VUTwOL+nIP9E06O1wD5d9U+VG+H6V+E9UmT/XEoLwqZz9Rt8Uso9w3cj2+wJ/Iaq5oe5GBReXYrxbCg/dbHY+HKgPUrYqYzdVv895QTwLp8w/15Xn6j3hUW2Idazkn6d7qXDaZu4HCWFzOdqdvij1ROADl/X7Xm5fdIjH+5GEBd4Rz58+9oq8EKrgw4ap2Y6UzdFj84WjUB9A3iGfA/83I7ZDnGwV32od52jv6Nt1dcAZwdKuyb0E5ix8eqJoDaKZ4Rv4SX10+DHMxmH+rbTpG/pGSbjcGB9yUPiJrOrxM7fkM5AeS5xZ47kpfX34M89LYPtZFD9O9UMZtEAcuA/QNETeesxJ7vVk4AM3KM+R7cvJ6JcZBFD93ZjpC/XPZHjO/Hod0rbDpTt8Vvq5wARufmFvSexdy8Po/x8DCD9Ql2gPwlXrf/pACWAetfCpvO1JvbiWHKKSC/gTsHufncDXT/m8EcrY8doH8RDAqmkWXAQV7ipjN1W/xzyglgC25xn4Wbz11BHhaxD/WA8vJXis2oCVwZsNCdkanb4n+rnAC+yyvwU/i1FO8CcrG+fahLFZe/HWvZhAGwDJhbAw8jRl3i5KecAMa15BT62fj5fBbjYS0GA9/HKy1/E57yYbSkwDLg3D4CpzP1tdGmygkgSg3+ow4crxLMwLhYlsEZyA6l5a8bqxUFlgHry4ROZ+LCoSgv5QSQ0y4TxzA6DnLxOgOs8W839igqf126sVvSfUDgR4VOZ+q2+B8pJ4Bv8In/J/l5/CzIxTls4K4qPEM9+QtnKX/IMmC9SazQ6UzdFv8F9b6BB/NIgHwcHUa1wHmD2T5t2oQ2Ssnf+bW+TFe0LxB7gtjZTN0Wv6Z6Ashl5kRWjg4fxLhYmWUD8TP7ciQqon6eAsw/o7YB4d8VO5tHEO+YlFFPAL/kkARNOJ6kxVTG+Mj6oveI7yNC5Ze/4NfZFwABy4D18BDB05m6Lf5x5QSwGYc06M/R38UgH1eyhz6w3hh/qeWvyv4RBCsKLAPWp4qeztRt8Qeo9wp4BJ4HUZk4ulsP5OSzNEfYRX6S9T0wd7v6JJQgy4DFrwWjbov/q3oCuBueClzbabwGcpLsW2HgtTt95JO/ft9HEvFxFOjFZOEnQ1L3d+qpngCmhycD16tUVzA+plD6ENvhzZxSffo+RzhGbS/QkXnCZ7Mv8WHZOOWGA2sj0JNnZ/DcSF5TC+PkWWpHqq89O0oK9SsTmJaSB2QZsAw94anb4mdSTgBR7fH+si48ne2rkJNrtnx4XvC56XmnZiR+ZQCWAetVJMhm6rb4W9UTwOzYnEgawdPZRSAvfwD5E+ndsa2gIhjVNjkN/ScfsAxYHypBNlO3xW+kngDexObFIa7OLsM4Gdob6FPFotfHi1Yy3KZk1swI35FlwHpqCbKZui3+bPUEMBJbZFCBq7PTME6ehi9i2v1jmggifv5tO3qvAfkNLAPWr8iQzXHEUy7UGw6saZ2g9yG4ujoQ9Kb0KQ/nQsYu2XaE86tg1M7+6brhXN6I9K2FFNk8kpiFXuoJYJr0QDvGVwBBXvKb+OTVtZrf+FAu4jcqxydDwA3juiNDd4QU2VyBmIVmmmuuCW4+5y7NbIucKOdf+9Mb3eNc4l1zzTVRLJX3ooT55FUjo+YnLErr47LtmmuuCWg1hlR7rWpTio4ppWd/sH/AbZdh11xzTXCLSZV2zuptI8uwOPEP3bGw4MprG8+4rLrmmmtyWeaxQ+q90OP5/IfNdpmOmlBlZJ3AFiWar4t2WXTNNdckt7CA1BW8389avnCPQ8MjIn5q27ZU3rx5W+b5wwb/9meVtp0iLryzu+PQ8t9naH2rYozLmGuuueaaa462/wFuTqITtlmi+QAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyNi0wOC0zMVQxNDozNTo0MSswMDowMIp+yAkAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjYtMDgtMzFUMTQ6MzU6NDErMDA6MDD7I3C1AAAAKHRFWHRkYXRlOnRpbWVzdGFtcAAyMDI2LTA4LTMxVDE0OjM1OjQxKzAwOjAwrDZRagAAAABJRU5ErkJggg==";
|