canvas-globe 1.1.0 → 1.2.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/CHANGELOG.md +30 -0
- package/README.md +76 -4
- package/THIRD_PARTY_NOTICES.md +1 -0
- package/codemeta.json +1 -1
- package/dist/canvas-globe.umd.js +182 -6
- package/package.json +34 -2
- package/src/charts.js +458 -0
- package/src/controls.js +187 -0
- package/src/data/places.js +5 -0
- package/src/fx/effects/ambient.js +355 -0
- package/src/fx/effects/data.js +438 -0
- package/src/fx/effects/entrances.js +111 -0
- package/src/fx/effects/interaction.js +850 -0
- package/src/fx/effects/scene.js +571 -0
- package/src/fx/effects/transitions.js +209 -0
- package/src/fx/index.js +52 -0
- package/src/fx/pointer.js +94 -0
- package/src/fx/runtime.js +161 -0
- package/src/geo-globe.js +170 -4
- package/src/license.js +11 -1
- package/src/places.js +97 -0
- package/src/recipes.js +188 -0
- package/src/version.js +1 -1
- package/types/charts.d.ts +82 -0
- package/types/controls.d.ts +54 -0
- package/types/fx-interaction.d.ts +16 -0
- package/types/fx.d.ts +313 -0
- package/types/index.d.ts +48 -0
- package/types/places.d.ts +43 -0
- package/types/recipes.d.ts +54 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
/** Ambient loops and post-processing passes. */
|
|
2
|
+
import { lerp, particles, scratch, releaseScratch, TAU } from "../runtime.js";
|
|
3
|
+
|
|
4
|
+
const alpha = (color, opacity) => {
|
|
5
|
+
if (typeof color !== "string") return `rgba(255,255,255,${opacity})`;
|
|
6
|
+
const hex = color.trim().match(/^#([\da-f]{3}|[\da-f]{6})$/i);
|
|
7
|
+
if (hex) {
|
|
8
|
+
const value = hex[1].length === 3
|
|
9
|
+
? hex[1].split("").map((c) => c + c).join("")
|
|
10
|
+
: hex[1];
|
|
11
|
+
const n = Number.parseInt(value, 16);
|
|
12
|
+
return `rgba(${n >> 16},${(n >> 8) & 255},${n & 255},${opacity})`;
|
|
13
|
+
}
|
|
14
|
+
if (/^\s*\d+\s*,/.test(color)) return `rgba(${color},${opacity})`;
|
|
15
|
+
const rgb = color.match(/^rgba?\(([^)]+)\)$/i);
|
|
16
|
+
if (rgb) return `rgba(${rgb[1].split(",").slice(0, 3).join(",")},${opacity})`;
|
|
17
|
+
return color;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* CRT lines plus a bright band travelling top to bottom.
|
|
22
|
+
* Ports catalogue effect H.
|
|
23
|
+
*/
|
|
24
|
+
export const scanlines = ({ duration = 3200, color = "#67e8f9", gap = 3, strength = 0.045 } = {}) => ({
|
|
25
|
+
name: "scanlines",
|
|
26
|
+
stage: "post",
|
|
27
|
+
duration,
|
|
28
|
+
frame(ctx, globe, t) {
|
|
29
|
+
const w = globe.canvas.clientWidth, h = globe.canvas.clientHeight;
|
|
30
|
+
ctx.globalCompositeOperation = "screen";
|
|
31
|
+
ctx.fillStyle = color;
|
|
32
|
+
ctx.globalAlpha = strength;
|
|
33
|
+
for (let y = 0; y < h; y += gap) ctx.fillRect(0, y, w, 1);
|
|
34
|
+
ctx.globalAlpha = 1;
|
|
35
|
+
const y = t * h;
|
|
36
|
+
const band = ctx.createLinearGradient(0, y - 40, 0, y + 40);
|
|
37
|
+
band.addColorStop(0, "transparent");
|
|
38
|
+
band.addColorStop(0.5, color);
|
|
39
|
+
band.addColorStop(1, "transparent");
|
|
40
|
+
ctx.globalAlpha = 0.22;
|
|
41
|
+
ctx.fillStyle = band;
|
|
42
|
+
ctx.fillRect(0, y - 40, w, 80);
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The rim glow swells and settles on a slow cycle.
|
|
48
|
+
* Ports catalogue effect F.
|
|
49
|
+
*/
|
|
50
|
+
export const breathe = ({ duration = 4000, color = "125,211,252", strength = 0.16 } = {}) => ({
|
|
51
|
+
name: "breathe",
|
|
52
|
+
stage: "post",
|
|
53
|
+
duration,
|
|
54
|
+
frame(ctx, globe, t) {
|
|
55
|
+
const w = globe.canvas.clientWidth, h = globe.canvas.clientHeight;
|
|
56
|
+
const cx = w / 2, cy = h / 2, r = Math.min(w, h) * 0.4;
|
|
57
|
+
const pulse = 0.5 + 0.5 * Math.sin(t * TAU);
|
|
58
|
+
const halo = ctx.createRadialGradient(cx, cy, r * 0.94, cx, cy, r * (1.14 + pulse * 0.1));
|
|
59
|
+
halo.addColorStop(0, `rgba(${color},0)`);
|
|
60
|
+
halo.addColorStop(0.45, `rgba(${color},${0.1 + pulse * strength})`);
|
|
61
|
+
halo.addColorStop(1, `rgba(${color},0)`);
|
|
62
|
+
ctx.fillStyle = halo;
|
|
63
|
+
ctx.fillRect(0, 0, w, h);
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Arc heads smear into trails that fade over about a second.
|
|
69
|
+
* Ports catalogue effect X.
|
|
70
|
+
*/
|
|
71
|
+
export const lightTrails = ({ duration = 4000, fade = 0.26, strength = 0.32 } = {}) => ({
|
|
72
|
+
name: "lightTrails",
|
|
73
|
+
stage: "post",
|
|
74
|
+
duration,
|
|
75
|
+
frame(ctx, globe) {
|
|
76
|
+
const buf = scratch(globe, "lightTrails");
|
|
77
|
+
if (!buf) return;
|
|
78
|
+
const { canvas } = globe;
|
|
79
|
+
buf.ctx.globalCompositeOperation = "source-over";
|
|
80
|
+
buf.ctx.fillStyle = `rgba(0,0,0,${fade})`;
|
|
81
|
+
buf.ctx.fillRect(0, 0, buf.canvas.width, buf.canvas.height);
|
|
82
|
+
buf.ctx.globalCompositeOperation = "lighter";
|
|
83
|
+
buf.ctx.globalAlpha = 0.5;
|
|
84
|
+
buf.ctx.drawImage(canvas, 0, 0);
|
|
85
|
+
buf.ctx.globalAlpha = 1;
|
|
86
|
+
|
|
87
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
88
|
+
ctx.globalCompositeOperation = "lighter";
|
|
89
|
+
ctx.globalAlpha = strength;
|
|
90
|
+
ctx.drawImage(buf.canvas, 0, 0);
|
|
91
|
+
},
|
|
92
|
+
dispose(_state, globe) {
|
|
93
|
+
releaseScratch(globe, "lightTrails");
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The world is dark except a soft circle that follows the pointer.
|
|
99
|
+
* Ports catalogue effect CB.
|
|
100
|
+
*/
|
|
101
|
+
export const torch = ({ radius = 74, darkness = 0.9, ring = true } = {}) => ({
|
|
102
|
+
name: "torch",
|
|
103
|
+
stage: "post",
|
|
104
|
+
duration: 1000,
|
|
105
|
+
frame(ctx, globe) {
|
|
106
|
+
const buf = scratch(globe, "torch");
|
|
107
|
+
if (!buf) return;
|
|
108
|
+
const w = globe.canvas.clientWidth, h = globe.canvas.clientHeight;
|
|
109
|
+
const dpr = globe.canvas.width / Math.max(1, w);
|
|
110
|
+
const { x, y, over } = globe.pointer;
|
|
111
|
+
|
|
112
|
+
buf.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
113
|
+
buf.ctx.globalCompositeOperation = "source-over";
|
|
114
|
+
buf.ctx.fillStyle = `rgba(4,6,12,${darkness})`;
|
|
115
|
+
buf.ctx.fillRect(0, 0, w, h);
|
|
116
|
+
if (over) {
|
|
117
|
+
buf.ctx.globalCompositeOperation = "destination-out";
|
|
118
|
+
const hole = buf.ctx.createRadialGradient(x, y, 0, x, y, radius);
|
|
119
|
+
hole.addColorStop(0, "rgba(0,0,0,1)");
|
|
120
|
+
hole.addColorStop(0.62, "rgba(0,0,0,.92)");
|
|
121
|
+
hole.addColorStop(1, "rgba(0,0,0,0)");
|
|
122
|
+
buf.ctx.fillStyle = hole;
|
|
123
|
+
buf.ctx.fillRect(x - radius - 8, y - radius - 8, radius * 2 + 16, radius * 2 + 16);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
127
|
+
ctx.drawImage(buf.canvas, 0, 0);
|
|
128
|
+
if (over && ring) {
|
|
129
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
130
|
+
ctx.strokeStyle = "rgba(255,255,255,.22)";
|
|
131
|
+
ctx.beginPath();
|
|
132
|
+
ctx.arc(x, y, radius, 0, TAU);
|
|
133
|
+
ctx.stroke();
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
dispose(_state, globe) {
|
|
137
|
+
releaseScratch(globe, "torch");
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Four coloured light sources drifting behind the sphere.
|
|
143
|
+
* Ports catalogue effect BE.
|
|
144
|
+
*/
|
|
145
|
+
export const meshGradient = ({
|
|
146
|
+
duration = 11000,
|
|
147
|
+
colors = ["#7c3aed", "#06b6d4", "#ec4899", "#f59e0b"],
|
|
148
|
+
background = "#06080f",
|
|
149
|
+
} = {}) => ({
|
|
150
|
+
name: "meshGradient",
|
|
151
|
+
stage: "beneath",
|
|
152
|
+
duration,
|
|
153
|
+
frame(ctx, globe, t) {
|
|
154
|
+
const w = globe.canvas.clientWidth, h = globe.canvas.clientHeight;
|
|
155
|
+
ctx.fillStyle = background;
|
|
156
|
+
ctx.fillRect(0, 0, w, h);
|
|
157
|
+
const spots = [[0.18, 0.22], [0.78, 0.3], [0.3, 0.8], [0.8, 0.78]];
|
|
158
|
+
colors.forEach((color, i) => {
|
|
159
|
+
const a = t * TAU + i * 1.7;
|
|
160
|
+
const x = (spots[i % spots.length][0] + Math.sin(a) * 0.13) * w;
|
|
161
|
+
const y = (spots[i % spots.length][1] + Math.cos(a * 0.8) * 0.13) * h;
|
|
162
|
+
const r = Math.min(w, h) * (0.44 + 0.07 * Math.sin(a * 1.3));
|
|
163
|
+
const blob = ctx.createRadialGradient(x, y, 0, x, y, r);
|
|
164
|
+
blob.addColorStop(0, color);
|
|
165
|
+
blob.addColorStop(1, "rgba(0,0,0,0)");
|
|
166
|
+
ctx.fillStyle = blob;
|
|
167
|
+
ctx.fillRect(0, 0, w, h);
|
|
168
|
+
});
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Cities glow only once the terminator has rolled over them.
|
|
174
|
+
* Ports catalogue effect AR.
|
|
175
|
+
*/
|
|
176
|
+
export const cityLights = ({ duration = 9000, warm = "255,236,170" } = {}) => ({
|
|
177
|
+
name: "cityLights",
|
|
178
|
+
stage: "above",
|
|
179
|
+
duration,
|
|
180
|
+
frame(ctx, globe, t) {
|
|
181
|
+
const sun = { lon: lerp(-180, 180, 1 - t), lat: 12 };
|
|
182
|
+
ctx.globalCompositeOperation = "lighter";
|
|
183
|
+
const max = Math.max(1, ...globe.markers.map((m) => m.count || 1));
|
|
184
|
+
for (const m of globe.markers) {
|
|
185
|
+
const p = globe.project(m.lon, m.lat);
|
|
186
|
+
if (!p) continue;
|
|
187
|
+
const away = Math.abs(((m.lon - sun.lon + 540) % 360) - 180);
|
|
188
|
+
const night = Math.max(0, Math.min(1, (away - 78) / 34));
|
|
189
|
+
if (night <= 0.02) continue;
|
|
190
|
+
const r = (5 + ((m.count || 1) / max) * 13) * night;
|
|
191
|
+
const glow = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, r);
|
|
192
|
+
glow.addColorStop(0, `rgba(${warm},${0.95 * night})`);
|
|
193
|
+
glow.addColorStop(0.35, `rgba(255,186,90,${0.5 * night})`);
|
|
194
|
+
glow.addColorStop(1, "rgba(255,150,40,0)");
|
|
195
|
+
ctx.fillStyle = glow;
|
|
196
|
+
ctx.beginPath();
|
|
197
|
+
ctx.arc(p.x, p.y, r, 0, TAU);
|
|
198
|
+
ctx.fill();
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Three star layers drifting at different speeds, which reads as depth.
|
|
205
|
+
* Ports catalogue effect G.
|
|
206
|
+
*/
|
|
207
|
+
export const starfield = ({ duration = 9000, layers = 3, per = 40, seed = 5 } = {}) => ({
|
|
208
|
+
name: "starfield",
|
|
209
|
+
stage: "beneath",
|
|
210
|
+
z: -30,
|
|
211
|
+
duration,
|
|
212
|
+
setup() {
|
|
213
|
+
return {
|
|
214
|
+
bands: Array.from({ length: layers }, (_, i) =>
|
|
215
|
+
particles(per, seed + i, (r) => ({ x: r(), y: r(), s: 0.5 + i * 0.5 }))),
|
|
216
|
+
};
|
|
217
|
+
},
|
|
218
|
+
frame(ctx, globe, t, state) {
|
|
219
|
+
const w = globe.canvas.clientWidth, h = globe.canvas.clientHeight;
|
|
220
|
+
state.bands.forEach((stars, i) => {
|
|
221
|
+
ctx.fillStyle = `rgba(255,255,255,${0.2 + i * 0.22})`;
|
|
222
|
+
for (const s of stars) {
|
|
223
|
+
// Wrapping on the phase keeps the drift seamless across a loop.
|
|
224
|
+
const x = ((s.x + t * (0.02 + i * 0.03) * layers) % 1) * w;
|
|
225
|
+
ctx.beginPath();
|
|
226
|
+
ctx.arc(x, s.y * h, s.s * 0.8, 0, TAU);
|
|
227
|
+
ctx.fill();
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* A specular band sweeping across the sphere, like light off glass.
|
|
235
|
+
* Ports catalogue effect AG.
|
|
236
|
+
*/
|
|
237
|
+
export const shineSweep = ({ duration = 3200, width = 0.26, strength = 0.7 } = {}) => ({
|
|
238
|
+
name: "shineSweep",
|
|
239
|
+
stage: "above",
|
|
240
|
+
z: 30,
|
|
241
|
+
duration,
|
|
242
|
+
frame(ctx, globe, t) {
|
|
243
|
+
if (globe.o.mode !== "globe") return;
|
|
244
|
+
const w = globe.canvas.clientWidth, h = globe.canvas.clientHeight;
|
|
245
|
+
const cx = w / 2, cy = h / 2, r = globe._radius(w, h);
|
|
246
|
+
// Travel beyond both edges so the band leaves the sphere completely.
|
|
247
|
+
const centre = -1.45 + t * 2.9;
|
|
248
|
+
ctx.save();
|
|
249
|
+
ctx.beginPath();
|
|
250
|
+
ctx.arc(cx, cy, r, 0, TAU);
|
|
251
|
+
ctx.clip();
|
|
252
|
+
ctx.globalCompositeOperation = "lighter";
|
|
253
|
+
const from = cx + (centre - width) * r, to = cx + (centre + width) * r;
|
|
254
|
+
const band = ctx.createLinearGradient(from, cy + r * 0.8, to, cy - r * 0.8);
|
|
255
|
+
band.addColorStop(0, "rgba(255,255,255,0)");
|
|
256
|
+
band.addColorStop(0.36, `rgba(180,225,255,${strength * 0.18})`);
|
|
257
|
+
band.addColorStop(0.49, `rgba(255,255,255,${strength * 0.9})`);
|
|
258
|
+
band.addColorStop(0.54, `rgba(255,255,255,${strength})`);
|
|
259
|
+
band.addColorStop(0.66, `rgba(180,225,255,${strength * 0.2})`);
|
|
260
|
+
band.addColorStop(1, "rgba(255,255,255,0)");
|
|
261
|
+
ctx.fillStyle = band;
|
|
262
|
+
ctx.fillRect(cx - r, cy - r, r * 2, r * 2);
|
|
263
|
+
ctx.globalAlpha = strength * 0.7;
|
|
264
|
+
ctx.strokeStyle = "rgba(255,255,255,.9)";
|
|
265
|
+
ctx.lineWidth = 1.2;
|
|
266
|
+
ctx.beginPath();
|
|
267
|
+
ctx.arc(cx, cy, r - 1.2, -1.1, 0.2);
|
|
268
|
+
ctx.stroke();
|
|
269
|
+
ctx.restore();
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Glass-ball treatment: a bright upper cap, a dark lower rim and a thin edge.
|
|
275
|
+
* Ports catalogue effect AT.
|
|
276
|
+
*/
|
|
277
|
+
export const glassSphere = ({ tint = "#bde8ff", rim = 0.72, gloss = 0.58 } = {}) => ({
|
|
278
|
+
name: "glassSphere",
|
|
279
|
+
stage: "above",
|
|
280
|
+
z: 32,
|
|
281
|
+
duration: 1000,
|
|
282
|
+
frame(ctx, globe) {
|
|
283
|
+
if (globe.o.mode !== "globe") return;
|
|
284
|
+
const w = globe.canvas.clientWidth, h = globe.canvas.clientHeight;
|
|
285
|
+
const cx = w / 2, cy = h / 2, r = globe._radius(w, h);
|
|
286
|
+
ctx.save();
|
|
287
|
+
ctx.beginPath();
|
|
288
|
+
ctx.arc(cx, cy, r, 0, TAU);
|
|
289
|
+
ctx.clip();
|
|
290
|
+
const cap = ctx.createRadialGradient(cx - r * 0.35, cy - r * 0.45, 0, cx - r * 0.35, cy - r * 0.45, r * 1.1);
|
|
291
|
+
cap.addColorStop(0, alpha(tint, gloss));
|
|
292
|
+
cap.addColorStop(0.28, alpha(tint, gloss * 0.34));
|
|
293
|
+
cap.addColorStop(0.58, alpha(tint, 0));
|
|
294
|
+
ctx.fillStyle = cap;
|
|
295
|
+
ctx.fillRect(cx - r, cy - r, r * 2, r * 2);
|
|
296
|
+
const base = ctx.createRadialGradient(cx, cy + r * 0.6, r * 0.1, cx, cy + r * 0.2, r);
|
|
297
|
+
base.addColorStop(0, "rgba(0,0,0,.48)");
|
|
298
|
+
base.addColorStop(0.72, "rgba(0,0,0,.08)");
|
|
299
|
+
base.addColorStop(1, "rgba(0,0,0,0)");
|
|
300
|
+
ctx.fillStyle = base;
|
|
301
|
+
ctx.fillRect(cx - r, cy - r, r * 2, r * 2);
|
|
302
|
+
ctx.restore();
|
|
303
|
+
ctx.save();
|
|
304
|
+
ctx.strokeStyle = alpha(tint, rim);
|
|
305
|
+
ctx.shadowColor = alpha(tint, 0.9);
|
|
306
|
+
ctx.shadowBlur = 14;
|
|
307
|
+
ctx.lineWidth = 2;
|
|
308
|
+
ctx.beginPath();
|
|
309
|
+
ctx.arc(cx, cy, r - 1, 0, TAU);
|
|
310
|
+
ctx.stroke();
|
|
311
|
+
ctx.shadowBlur = 0;
|
|
312
|
+
ctx.lineCap = "round";
|
|
313
|
+
ctx.lineWidth = Math.max(2, r * 0.018);
|
|
314
|
+
ctx.strokeStyle = "rgba(255,255,255,.72)";
|
|
315
|
+
ctx.beginPath();
|
|
316
|
+
ctx.arc(cx, cy, r * 0.82, -2.65, -1.78);
|
|
317
|
+
ctx.stroke();
|
|
318
|
+
ctx.restore();
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Mains-hum flicker on a coloured bloom, as a neon sign does.
|
|
324
|
+
* Ports catalogue effect BG.
|
|
325
|
+
*/
|
|
326
|
+
export const neonFlicker = ({ duration = 2400, color = "#ec4899", strength = 0.72, seed = 9 } = {}) => ({
|
|
327
|
+
name: "neonFlicker",
|
|
328
|
+
stage: "post",
|
|
329
|
+
duration,
|
|
330
|
+
setup() {
|
|
331
|
+
return { steps: particles(48, seed, (r) => (r() < 0.16 ? 0.25 + r() * 0.4 : 0.85 + r() * 0.15)) };
|
|
332
|
+
},
|
|
333
|
+
frame(ctx, globe, t, state) {
|
|
334
|
+
const w = globe.canvas.clientWidth, h = globe.canvas.clientHeight;
|
|
335
|
+
const dpr = globe.canvas.width / Math.max(1, w);
|
|
336
|
+
const cx = w / 2, cy = h / 2;
|
|
337
|
+
const r = globe.o.mode === "globe" ? globe._radius(w, h) : Math.min(w, h) * 0.42;
|
|
338
|
+
const level = state.steps[Math.floor(t * state.steps.length) % state.steps.length];
|
|
339
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
340
|
+
ctx.globalCompositeOperation = "lighter";
|
|
341
|
+
ctx.fillStyle = alpha(color, strength * level * 0.055);
|
|
342
|
+
ctx.fillRect(0, 0, w, h);
|
|
343
|
+
ctx.globalAlpha = 0.45 + level * 0.55;
|
|
344
|
+
ctx.strokeStyle = alpha(color, Math.min(1, strength * 0.95));
|
|
345
|
+
ctx.shadowColor = alpha(color, 1);
|
|
346
|
+
ctx.shadowBlur = 12 + strength * 28;
|
|
347
|
+
ctx.lineWidth = 1.5 + strength * 2.5;
|
|
348
|
+
ctx.beginPath();
|
|
349
|
+
ctx.arc(cx, cy, r + 2, 0, TAU);
|
|
350
|
+
ctx.stroke();
|
|
351
|
+
ctx.globalAlpha = 1;
|
|
352
|
+
ctx.shadowBlur = 0;
|
|
353
|
+
ctx.globalCompositeOperation = "source-over";
|
|
354
|
+
},
|
|
355
|
+
});
|