diff --git a/lib/scratch-card.js b/lib/scratch-card.js index 82debb0..5d937be 100644 --- a/lib/scratch-card.js +++ b/lib/scratch-card.js @@ -1,9 +1,11 @@ class ScratchCard extends HTMLElement { - canvas = document.createElement('canvas'); - span = document.createElement('span'); - ctx = this.canvas.getContext('2d', { willReadFrequently: true }); - pointer = {x: 0, y: 0}; - idleTimer = 0; + canvas = document.createElement('canvas'); + particleCanvas = document.createElement('canvas'); + span = document.createElement('span'); + ctx = this.canvas.getContext('2d', { willReadFrequently: true }); + pointer = {x: 0, y: 0}; + idleTimer = 0; + worker = new Worker(new URL('scratch-particle.js', import.meta.url), {type: 'module'}); static observedAttributes = [ 'code', @@ -108,13 +110,16 @@ class ScratchCard extends HTMLElement { e.stopPropagation(); }); - const link = document.createElement('link'); + const link = document.createElement('link'), + offscreen = this.particleCanvas.transferControlToOffscreen(); link.rel = 'stylesheet'; link.type = 'text/css'; link.href = new URL('./scratch-card.css', import.meta.url); - this.shadowRoot.append(link, this.span, this.canvas); + this.shadowRoot.append(link, this.span, this.canvas, this.particleCanvas); + + this.worker.postMessage(offscreen, [offscreen]); } updatePercentageScratched() { @@ -131,6 +136,31 @@ class ScratchCard extends HTMLElement { this.#drawFoil(); } + particles(x, y) { + const image = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height); + + x = Math.floor(x); + y = Math.floor(y); + + const i = (y * this.canvas.width + x) * 4 + 4, + i2 = (y * this.canvas.width + (x + 1)) * 4 + 4, + i3 = ((y + 1) * this.canvas.width + x) * 4 + 4, + i4 = ((y + 1) * this.canvas.width + (x + 1)) * 4 + 4; + + if (image.data[i] > 50 && image.data[i2] > 50 && image.data[i3] > 50 && image.data[i4] > 50) { + this.worker.postMessage({ + position: {x, y}, + scale: window.devicePixelRatio, + fill: this.scratchFill, + offset: this.scratchSize, + size: { + width: this.canvas.width, + height: this.canvas.height + } + }); + } + } + draw(x, y) { this.ctx.shadowColor = 'white'; this.ctx.fillStyle = 'white'; @@ -163,6 +193,8 @@ class ScratchCard extends HTMLElement { p.y + this.random(this.scratchSize) ); } + + this.particles(p.x, p.y); this.ctx.closePath(); this.ctx.fill('evenodd'); diff --git a/lib/scratch-particle.js b/lib/scratch-particle.js new file mode 100644 index 0000000..a4e0252 --- /dev/null +++ b/lib/scratch-particle.js @@ -0,0 +1,108 @@ +let canvas, settings = {width: 0, height: 0, scale: 1}; + +/** @type { CanvasRenderingContext2D } */ +let ctx; + +/** + * @type {{ + * created: Date, + * lifetime: number, + * speed: number, + * fill: string, + * position: { + * x: number, + * y: number, + * }, + * velocity: { + * x: number, + * y: number, + * } + * }[]} + */ +const particles = []; + +const update = () => { + const now = new Date(); + + Object.assign(canvas, settings); + ctx.scale(settings.scale, settings.scale); + + for (let i = 0, p, pec; i < particles.length; i++) { + p = particles[i]; + + if ((now - p.created) > p.lifetime) { + particles.splice(i--, 1); + + continue; + } + + p.position.x += p.velocity.x * p.speed; + p.position.y += p.velocity.y * p.speed; + + p.velocity.x *= 0.98; + p.velocity.y *= 0.98; + + pec = 1 - ((now - p.created) / p.lifetime); + + ctx.beginPath(); + + ctx.fillStyle = p.fill; + + const fill = parseInt(ctx.fillStyle.substring(1), 16), + r = (fill >> 16) & 0xFF, + g = (fill >> 8) & 0xFF, + b = fill & 0xFF, + fac = 0.6; + + ctx.fillStyle = `rgb(${r * fac}, ${g * fac}, ${b * fac})`; + ctx.globalAlpha = pec; + + ctx.fillRect(p.position.x, p.position.y, 1, 1); + + ctx.closePath(); + } + + if (particles.length) { + requestAnimationFrame(update); + } +} + +onmessage = (message) => { + + if (message.data instanceof OffscreenCanvas) { + canvas = message.data; + ctx = canvas.getContext('2d'); + return; + } + + const particleCount = 5, + wasEmpty = particles.length == 0; + + Object.assign(settings, message.data.size); + + settings.scale = message.data.scale; + + for (let i = 0; i < particleCount; i++) { + + const dir = { + x: -0.5 + Math.random(), + y: -0.5 + Math.random(), + }; + + particles.push({ + created: new Date(), + lifetime: 1000, + speed: Math.random(), + fill: message.data.fill, + position: { + x: message.data.position.x + dir.x * message.data.offset, + y: message.data.position.y + dir.y * message.data.offset + }, + velocity: dir + }); + } + + if (wasEmpty) { + update(); + } +}