scratch-reveal 1.1.0 → 1.2.1-dev.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/dist/index.cjs.js CHANGED
@@ -1,4 +1,4 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});class z{ctx;mouseX;mouseY;constructor(t,e,s){this.ctx=t,this.mouseX=e,this.mouseY=s}updateMousePosition(t,e){this.mouseX=t,this.mouseY=e}brush(t,e=0){if(!t)return;const s=Math.atan2(this.mouseY,this.mouseX);if(this.ctx.save(),this.ctx.translate(this.mouseX,this.mouseY),this.ctx.rotate(s),e>0){const i=e,n=e*(t.height/t.width);this.ctx.drawImage(t,-(i/2),-(n/2),i,n)}else this.ctx.drawImage(t,-(t.width/2),-(t.height/2));this.ctx.restore()}}function v(h){return new Promise((t,e)=>{const s=new Image;s.crossOrigin="anonymous",s.onload=()=>t(s),s.onerror=()=>e(new Error(`Image ${h} failed to load`)),s.src=h})}function E(h){let t=0;return((...s)=>{t||(t=requestAnimationFrame(()=>{t=0,h(...s)}))})}const b=`.sr {
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});class M{ctx;mouseX;mouseY;constructor(t,e,s){this.ctx=t,this.mouseX=e,this.mouseY=s}updateMousePosition(t,e){this.mouseX=t,this.mouseY=e}brush(t,e=0){if(!t)return;const s=Math.atan2(this.mouseY,this.mouseX);if(this.ctx.save(),this.ctx.translate(this.mouseX,this.mouseY),this.ctx.rotate(s),e>0){const i=e,h=e*(t.height/t.width);this.ctx.drawImage(t,-(i/2),-(h/2),i,h)}else this.ctx.drawImage(t,-(t.width/2),-(t.height/2));this.ctx.restore()}}function v(r){return new Promise((t,e)=>{const s=new Image;s.crossOrigin="anonymous",s.onload=()=>t(s),s.onerror=()=>e(new Error(`Image ${r} failed to load`)),s.src=r})}function E(r){let t=0;return((...s)=>{t||(t=requestAnimationFrame(()=>{t=0,r(...s)}))})}const p=`.sr {
2
2
  position: relative;
3
3
  overflow: hidden;
4
4
  width: 100%;
@@ -11,6 +11,10 @@
11
11
  width: 100%;
12
12
  height: 100%;
13
13
  object-fit: cover;
14
+ user-select: none;
15
+ -webkit-user-select: none;
16
+ -webkit-user-drag: none;
17
+ pointer-events: none;
14
18
  }
15
19
 
16
20
  .sr__canvas {
@@ -18,6 +22,10 @@
18
22
  inset: 0;
19
23
  width: 100%;
20
24
  height: 100%;
25
+ touch-action: none;
26
+ user-select: none;
27
+ -webkit-user-select: none;
28
+ -webkit-tap-highlight-color: transparent;
21
29
  }
22
30
 
23
31
  scratch-reveal {
@@ -25,4 +33,4 @@ scratch-reveal {
25
33
  width: 100%;
26
34
  height: 100%;
27
35
  }
28
- `,C=b;function R(h){return"adoptedStyleSheets"in h}function P(h){if(typeof CSSStyleSheet>"u")return null;try{const t=new CSSStyleSheet;return t.replaceSync(h),t}catch{return null}}const _=P(b),u={width:300,height:300,brushSrc:"",imageMaskSrc:"",imageBackgroundSrc:"",brushSize:0,percentToFinish:60};class I{config;ctx;container;_canvas;brush;maskImage;backgroundImage;brushImage;backgroundEl;brushSize=0;percent=0;done=!1;completing=!1;destroyed=!1;lastProgressEmitMs=0;lastScratchX;lastScratchY;removeListeners;get canvas(){return this._canvas}constructor(t,e={}){if(this.config={...u,...e},this.container=typeof t=="string"?document.querySelector(t):t,!this.container)throw new Error("ScratchReveal: container not found");this._canvas=this.createCanvas(this.config.width,this.config.height),this.ctx=this._canvas.getContext("2d",{willReadFrequently:!0}),this.brush=new z(this.ctx,0,0),this.brushSize=this.config.brushSize,this.container.appendChild(this._canvas)}async init(){if(!this.config.brushSrc)throw new Error('ScratchReveal: "brushSrc" is required');if(!this.config.imageMaskSrc)throw new Error('ScratchReveal: "imageMaskSrc" is required');if(!this.config.imageBackgroundSrc)throw new Error('ScratchReveal: "imageBackgroundSrc" is required');const[t,e,s]=await Promise.all([v(this.config.brushSrc),v(this.config.imageMaskSrc),v(this.config.imageBackgroundSrc)]);return this.destroyed?this:(this.brushImage=t,this.maskImage=e,this.backgroundImage=s,this.drawMask(),this.setBackground(),this.bindEvents(),this)}destroy(){this.destroyed=!0,this.removeListeners?.()}getPercent(){return this.percent}createCanvas(t,e){const s=document.createElement("canvas");return s.className="sr__canvas",s.width=t,s.height=e,s.style.width="100%",s.style.height="100%",s}resize(t,e){this.destroyed||this.done||t<=0||e<=0||this._canvas.width===t&&this._canvas.height===e||(this._canvas.width=t,this._canvas.height=e,this.percent=0,this.ctx.globalCompositeOperation="source-over",this.drawMask())}setBrushSize(t){this.destroyed||!Number.isFinite(t)||t<0||(this.brushSize=t)}bindEvents(){const e=E((r,a)=>{this.percent=this.updatePercent();const o=performance.now();(!this.lastProgressEmitMs||o-this.lastProgressEmitMs>=120)&&(this.lastProgressEmitMs=o,this.config.onProgress?.(this.percent)),this.finish(r,a)}),s=E(r=>{this.updatePosition(r),this.scratch(),e(r,s)}),i=r=>{r.preventDefault(),this.updatePosition(r),this.scratch(),this._canvas.setPointerCapture(r.pointerId),this._canvas.addEventListener("pointermove",s),this.resetScratchTrail(),this.percent=this.updatePercent(),this.config.onProgress?.(this.percent),this.finish(r,s)},n=r=>{this.resetScratchTrail(),this.percent=this.updatePercent(),this.config.onProgress?.(this.percent),this.finish(r,s)};this._canvas.addEventListener("pointerdown",i),this._canvas.addEventListener("pointerup",n),this._canvas.addEventListener("pointerleave",n),this.removeListeners=()=>{this._canvas.removeEventListener("pointerdown",i),this._canvas.removeEventListener("pointerup",n),this._canvas.removeEventListener("pointerleave",n),this._canvas.removeEventListener("pointermove",s)}}updatePosition(t){const e=this._canvas.getBoundingClientRect(),s=e.width?this._canvas.width/e.width:1,i=e.height?this._canvas.height/e.height:1,n=(t.clientX-e.left)*s,r=(t.clientY-e.top)*i;this.brush.updateMousePosition(n,r)}drawMask(){this.maskImage&&(this.ctx.globalCompositeOperation="source-over",this.ctx.clearRect(0,0,this._canvas.width,this._canvas.height),this.ctx.drawImage(this.maskImage,0,0,this._canvas.width,this._canvas.height))}setBackground(){if(this.destroyed||!this.backgroundImage||!this.container.contains(this._canvas))return;const t=this.backgroundEl??document.createElement("img");t.src=this.backgroundImage.src,t.className="sr__bg",t.isConnected||this.container.insertBefore(t,this._canvas),this.backgroundEl=t}scratch(){if(!this.brushImage)return;const t=this.brushSize>0?this.brushSize:Math.max(1,Math.min(this.brushImage.width,this.brushImage.height)),e=Math.max(2,t*.4);if(this.lastScratchX!==void 0&&this.lastScratchY!==void 0){const i=this.brush.mouseX-this.lastScratchX,n=this.brush.mouseY-this.lastScratchY;if(i*i+n*n<e*e)return}const s=this.brushSize>0?this.brushSize:0;this.ctx.globalCompositeOperation="destination-out",this.ctx.save(),this.brush.brush(this.brushImage,s),this.ctx.restore(),this.lastScratchX=this.brush.mouseX,this.lastScratchY=this.brush.mouseY}resetScratchTrail(){this.lastScratchX=void 0,this.lastScratchY=void 0}updatePercent(){const e=this.ctx.getImageData(0,0,this._canvas.width,this._canvas.height).data;let s=0;for(let i=3;i<e.length;i+=4)e[i]===0&&s++;return s/(this._canvas.width*this._canvas.height)*100}finish(t,e){if(!this.done&&this.percent>this.config.percentToFinish&&(this.done=!0,this._canvas.style.pointerEvents="none",this.config.onComplete?.(),this.playCompleteEffect(),t&&e)){try{this._canvas.releasePointerCapture(t.pointerId)}catch{}this._canvas.removeEventListener("pointermove",e)}}playCompleteEffect(){if(this.destroyed||this.completing)return;this.completing=!0;const t=350,e="ease-out";this._canvas.style.transition=`opacity ${t}ms ${e}`,this._canvas.style.opacity||(this._canvas.style.opacity="1"),requestAnimationFrame(()=>{this._canvas.style.opacity="0",window.setTimeout(()=>{this.destroyed||this.clear()},t)})}clear(){this.ctx.clearRect(0,0,this._canvas.width,this._canvas.height)}}function y(h="scratch-reveal"){if(typeof window>"u"||!("customElements"in window)||customElements.get(h))return;function t(s,i,n,r){if(!s)return r;const a=s.trim();if(!a)return r;const o=Number.parseFloat(a.endsWith("%")?a.slice(0,-1):a.endsWith("px")?a.slice(0,-2):a);if(!Number.isFinite(o))return r;if(a.endsWith("px"))return Math.max(0,o);const d=Math.min(i,n);return Math.max(0,d*o/100)}class e extends HTMLElement{instance;container;styleEl=null;rebuildScheduled=!1;resizeObserver;lastErrorMessage=null;static get observedAttributes(){return["width","height","complete-percent","brush-src","brush-size","mask-src","background-src"]}constructor(){super();const i=this.attachShadow({mode:"open"});R(i)&&_?i.adoptedStyleSheets=[_]:(this.styleEl=document.createElement("style"),this.styleEl.textContent=b,i.append(this.styleEl)),this.container=document.createElement("div"),this.container.className="sr",i.append(this.container)}connectedCallback(){this.scheduleRebuild()}disconnectedCallback(){this.instance?.destroy(),this.instance=void 0,this.resizeObserver?.disconnect(),this.resizeObserver=void 0}attributeChangedCallback(i,n,r){n!==r&&this.scheduleRebuild()}scheduleRebuild(){this.rebuildScheduled||(this.rebuildScheduled=!0,queueMicrotask(()=>{this.rebuildScheduled=!1,this.isConnected&&this.rebuild()}))}rebuild(){this.container.replaceChildren(),this.instance?.destroy();const i=this.hasAttribute("width"),n=this.hasAttribute("height"),r=this.getBoundingClientRect(),a=Math.round(r.width),o=Math.round(r.height),d=i?Number(this.getAttribute("width")):a||u.width,m=n?Number(this.getAttribute("height")):o||u.height,k=Number(this.getAttribute("complete-percent")??u.percentToFinish),f=(this.getAttribute("brush-src")??"").trim(),x=t(this.getAttribute("brush-size"),d,m,u.brushSize),p=(this.getAttribute("mask-src")??"").trim(),S=(this.getAttribute("background-src")??"").trim(),l=[];if(f||l.push("brush-src"),p||l.push("mask-src"),S||l.push("background-src"),l.length){const c=`ScratchReveal: missing required attribute(s): ${l.join(", ")}`;this.renderError(c);return}i?this.container.style.width=`${d}px`:this.container.style.width="100%",n?this.container.style.height=`${m}px`:this.container.style.height="100%",this.instance=new I(this.container,{width:d,height:m,percentToFinish:k,brushSrc:f,brushSize:x,imageMaskSrc:p,imageBackgroundSrc:S,onProgress:c=>{this.dispatchEvent(new CustomEvent("progress",{detail:{percent:c}}))},onComplete:()=>{this.dispatchEvent(new CustomEvent("complete",{detail:{percent:100}}))}}),this.instance.init().catch(c=>{const g=c instanceof Error?c.message:"ScratchReveal: init failed";this.renderError(g)}),(!i||!n)&&"ResizeObserver"in window?(this.resizeObserver?.disconnect(),this.resizeObserver=new ResizeObserver(()=>{if(this.hasAttribute("width")&&this.hasAttribute("height")){this.resizeObserver?.disconnect(),this.resizeObserver=void 0;return}const c=this.getBoundingClientRect(),g=Math.round(c.width),w=Math.round(c.height);this.instance?.resize(g,w);const M=t(this.getAttribute("brush-size"),g,w,u.brushSize);this.instance?.setBrushSize(M)}),this.resizeObserver.observe(this)):(this.resizeObserver?.disconnect(),this.resizeObserver=void 0)}renderError(i){this.instance?.destroy(),this.instance=void 0,this.lastErrorMessage!==i&&(this.lastErrorMessage=i,this.dispatchEvent(new CustomEvent("error",{detail:{message:i}})))}}customElements.define(h,e)}function A(h){y(),h.config.globalProperties.$scratchReveal=!0}exports.installScratchReveal=A;exports.registerScratchRevealElement=y;exports.scratchRevealCssText=C;
36
+ `,R=p;function z(r){return"adoptedStyleSheets"in r}function P(r){if(typeof CSSStyleSheet>"u")return null;try{const t=new CSSStyleSheet;return t.replaceSync(r),t}catch{return null}}const _=P(p),u={width:300,height:300,brushSrc:"",imageMaskSrc:"",imageBackgroundSrc:"",brushSize:0,percentToFinish:60};class A{config;ctx;container;_canvas;brush;maskImage;backgroundImage;brushImage;backgroundEl;brushSize=0;percent=0;done=!1;completing=!1;destroyed=!1;lastProgressEmitMs=0;removeListeners;get canvas(){return this._canvas}constructor(t,e={}){if(this.config={...u,...e},this.container=typeof t=="string"?document.querySelector(t):t,!this.container)throw new Error("ScratchReveal: container not found");this._canvas=this.createCanvas(this.config.width,this.config.height),this.ctx=this._canvas.getContext("2d",{willReadFrequently:!0}),this.brush=new M(this.ctx,0,0),this.brushSize=this.config.brushSize,this.container.appendChild(this._canvas)}async init(){if(!this.config.brushSrc)throw new Error('ScratchReveal: "brushSrc" is required');if(!this.config.imageMaskSrc)throw new Error('ScratchReveal: "imageMaskSrc" is required');if(!this.config.imageBackgroundSrc)throw new Error('ScratchReveal: "imageBackgroundSrc" is required');const[t,e,s]=await Promise.all([v(this.config.brushSrc),v(this.config.imageMaskSrc),v(this.config.imageBackgroundSrc)]);return this.destroyed?this:(this.brushImage=t,this.maskImage=e,this.backgroundImage=s,this.drawMask(),this.setBackground(),this.bindEvents(),this)}destroy(){this.destroyed=!0,this.removeListeners?.()}getPercent(){return this.percent}createCanvas(t,e){const s=document.createElement("canvas");return s.className="sr__canvas",s.width=t,s.height=e,s.style.width="100%",s.style.height="100%",s}resize(t,e){this.destroyed||this.done||t<=0||e<=0||this._canvas.width===t&&this._canvas.height===e||(this._canvas.width=t,this._canvas.height=e,this.percent=0,this.ctx.globalCompositeOperation="source-over",this.drawMask())}setBrushSize(t){this.destroyed||!Number.isFinite(t)||t<0||(this.brushSize=t)}bindEvents(){const e=E((n,a)=>{this.percent=this.updatePercent();const o=performance.now();(!this.lastProgressEmitMs||o-this.lastProgressEmitMs>=120)&&(this.lastProgressEmitMs=o,this.config.onProgress?.(this.percent)),this.finish(n,a)}),s=E(n=>{(n.buttons&1)!==0&&(this.updatePosition(n),this.scratch(),e(n,s))}),i=n=>{try{this._canvas.releasePointerCapture(n.pointerId)}catch{}this._canvas.removeEventListener("pointermove",s),this.percent=this.updatePercent(),this.config.onProgress?.(this.percent),this.finish(n,s)},h=n=>{n.preventDefault(),this.updatePosition(n),this.scratch(),this._canvas.setPointerCapture(n.pointerId),this._canvas.addEventListener("pointermove",s),this.percent=this.updatePercent(),this.config.onProgress?.(this.percent),this.finish(n,s)};this._canvas.addEventListener("pointerdown",h),this._canvas.addEventListener("pointerup",i),this._canvas.addEventListener("pointercancel",i),this.removeListeners=()=>{this._canvas.removeEventListener("pointerdown",h),this._canvas.removeEventListener("pointerup",i),this._canvas.removeEventListener("pointercancel",i),this._canvas.removeEventListener("pointermove",s)}}updatePosition(t){const e=this._canvas.getBoundingClientRect(),s=e.width?this._canvas.width/e.width:1,i=e.height?this._canvas.height/e.height:1,h=(t.clientX-e.left)*s,n=(t.clientY-e.top)*i;this.brush.updateMousePosition(h,n)}drawMask(){this.maskImage&&(this.ctx.globalCompositeOperation="source-over",this.ctx.clearRect(0,0,this._canvas.width,this._canvas.height),this.ctx.drawImage(this.maskImage,0,0,this._canvas.width,this._canvas.height))}setBackground(){if(this.destroyed||!this.backgroundImage||!this.container.contains(this._canvas))return;const t=this.backgroundEl??document.createElement("img");t.src=this.backgroundImage.src,t.className="sr__bg",t.isConnected||this.container.insertBefore(t,this._canvas),this.backgroundEl=t}scratch(){if(!this.brushImage)return;const t=this.brushSize>0?this.brushSize:0;this.ctx.globalCompositeOperation="destination-out",this.ctx.save(),this.brush.brush(this.brushImage,t),this.ctx.restore()}updatePercent(){const e=this.ctx.getImageData(0,0,this._canvas.width,this._canvas.height).data;let s=0;for(let i=3;i<e.length;i+=4)e[i]===0&&s++;return s/(this._canvas.width*this._canvas.height)*100}finish(t,e){if(!this.done&&this.percent>this.config.percentToFinish&&(this.done=!0,this._canvas.style.pointerEvents="none",this.config.onComplete?.(),this.playCompleteEffect(),t&&e)){try{this._canvas.releasePointerCapture(t.pointerId)}catch{}this._canvas.removeEventListener("pointermove",e)}}playCompleteEffect(){if(this.destroyed||this.completing)return;this.completing=!0;const t=350,e="ease-out";this._canvas.style.transition=`opacity ${t}ms ${e}`,this._canvas.style.opacity||(this._canvas.style.opacity="1"),requestAnimationFrame(()=>{this._canvas.style.opacity="0",window.setTimeout(()=>{this.destroyed||this.clear()},t)})}clear(){this.ctx.clearRect(0,0,this._canvas.width,this._canvas.height)}}function y(r="scratch-reveal"){if(typeof window>"u"||!("customElements"in window)||customElements.get(r))return;function t(s,i,h,n){if(!s)return n;const a=s.trim();if(!a)return n;const o=Number.parseFloat(a.endsWith("%")?a.slice(0,-1):a.endsWith("px")?a.slice(0,-2):a);if(!Number.isFinite(o))return n;if(a.endsWith("px"))return Math.max(0,o);const d=Math.min(i,h);return Math.max(0,d*o/100)}class e extends HTMLElement{instance;container;styleEl=null;rebuildScheduled=!1;resizeObserver;lastErrorMessage=null;static get observedAttributes(){return["width","height","complete-percent","brush-src","brush-size","mask-src","background-src"]}constructor(){super();const i=this.attachShadow({mode:"open"});z(i)&&_?i.adoptedStyleSheets=[_]:(this.styleEl=document.createElement("style"),this.styleEl.textContent=p,i.append(this.styleEl)),this.container=document.createElement("div"),this.container.className="sr",i.append(this.container)}connectedCallback(){this.scheduleRebuild()}disconnectedCallback(){this.instance?.destroy(),this.instance=void 0,this.resizeObserver?.disconnect(),this.resizeObserver=void 0}attributeChangedCallback(i,h,n){h!==n&&this.scheduleRebuild()}scheduleRebuild(){this.rebuildScheduled||(this.rebuildScheduled=!0,queueMicrotask(()=>{this.rebuildScheduled=!1,this.isConnected&&this.rebuild()}))}rebuild(){this.container.replaceChildren(),this.instance?.destroy();const i=this.hasAttribute("width"),h=this.hasAttribute("height"),n=this.getBoundingClientRect(),a=Math.round(n.width),o=Math.round(n.height),d=i?Number(this.getAttribute("width")):a||u.width,m=h?Number(this.getAttribute("height")):o||u.height,k=Number(this.getAttribute("complete-percent")??u.percentToFinish),b=(this.getAttribute("brush-src")??"").trim(),x=t(this.getAttribute("brush-size"),d,m,u.brushSize),f=(this.getAttribute("mask-src")??"").trim(),S=(this.getAttribute("background-src")??"").trim(),l=[];if(b||l.push("brush-src"),f||l.push("mask-src"),S||l.push("background-src"),l.length){const c=`ScratchReveal: missing required attribute(s): ${l.join(", ")}`;this.renderError(c);return}i?this.container.style.width=`${d}px`:this.container.style.width="100%",h?this.container.style.height=`${m}px`:this.container.style.height="100%",this.instance=new A(this.container,{width:d,height:m,percentToFinish:k,brushSrc:b,brushSize:x,imageMaskSrc:f,imageBackgroundSrc:S,onProgress:c=>{this.dispatchEvent(new CustomEvent("progress",{detail:{percent:c}}))},onComplete:()=>{this.dispatchEvent(new CustomEvent("complete",{detail:{percent:100}}))}}),this.instance.init().catch(c=>{const g=c instanceof Error?c.message:"ScratchReveal: init failed";this.renderError(g)}),(!i||!h)&&"ResizeObserver"in window?(this.resizeObserver?.disconnect(),this.resizeObserver=new ResizeObserver(()=>{if(this.hasAttribute("width")&&this.hasAttribute("height")){this.resizeObserver?.disconnect(),this.resizeObserver=void 0;return}const c=this.getBoundingClientRect(),g=Math.round(c.width),w=Math.round(c.height);this.instance?.resize(g,w);const C=t(this.getAttribute("brush-size"),g,w,u.brushSize);this.instance?.setBrushSize(C)}),this.resizeObserver.observe(this)):(this.resizeObserver?.disconnect(),this.resizeObserver=void 0)}renderError(i){this.instance?.destroy(),this.instance=void 0,this.lastErrorMessage!==i&&(this.lastErrorMessage=i,this.dispatchEvent(new CustomEvent("error",{detail:{message:i}})))}}customElements.define(r,e)}function I(r){y(),r.config.globalProperties.$scratchReveal=!0}exports.installScratchReveal=I;exports.registerScratchRevealElement=y;exports.scratchRevealCssText=R;
package/dist/index.es.js CHANGED
@@ -1,4 +1,4 @@
1
- class M {
1
+ class C {
2
2
  ctx;
3
3
  mouseX;
4
4
  mouseY;
@@ -13,28 +13,28 @@ class M {
13
13
  return;
14
14
  const s = Math.atan2(this.mouseY, this.mouseX);
15
15
  if (this.ctx.save(), this.ctx.translate(this.mouseX, this.mouseY), this.ctx.rotate(s), e > 0) {
16
- const i = e, n = e * (t.height / t.width);
17
- this.ctx.drawImage(t, -(i / 2), -(n / 2), i, n);
16
+ const i = e, h = e * (t.height / t.width);
17
+ this.ctx.drawImage(t, -(i / 2), -(h / 2), i, h);
18
18
  } else
19
19
  this.ctx.drawImage(t, -(t.width / 2), -(t.height / 2));
20
20
  this.ctx.restore();
21
21
  }
22
22
  }
23
- function v(h) {
23
+ function v(r) {
24
24
  return new Promise((t, e) => {
25
25
  const s = new Image();
26
- s.crossOrigin = "anonymous", s.onload = () => t(s), s.onerror = () => e(new Error(`Image ${h} failed to load`)), s.src = h;
26
+ s.crossOrigin = "anonymous", s.onload = () => t(s), s.onerror = () => e(new Error(`Image ${r} failed to load`)), s.src = r;
27
27
  });
28
28
  }
29
- function E(h) {
29
+ function E(r) {
30
30
  let t = 0;
31
31
  return ((...s) => {
32
32
  t || (t = requestAnimationFrame(() => {
33
- t = 0, h(...s);
33
+ t = 0, r(...s);
34
34
  }));
35
35
  });
36
36
  }
37
- const b = `.sr {
37
+ const p = `.sr {
38
38
  position: relative;
39
39
  overflow: hidden;
40
40
  width: 100%;
@@ -47,6 +47,10 @@ const b = `.sr {
47
47
  width: 100%;
48
48
  height: 100%;
49
49
  object-fit: cover;
50
+ user-select: none;
51
+ -webkit-user-select: none;
52
+ -webkit-user-drag: none;
53
+ pointer-events: none;
50
54
  }
51
55
 
52
56
  .sr__canvas {
@@ -54,6 +58,10 @@ const b = `.sr {
54
58
  inset: 0;
55
59
  width: 100%;
56
60
  height: 100%;
61
+ touch-action: none;
62
+ user-select: none;
63
+ -webkit-user-select: none;
64
+ -webkit-tap-highlight-color: transparent;
57
65
  }
58
66
 
59
67
  scratch-reveal {
@@ -61,20 +69,20 @@ scratch-reveal {
61
69
  width: 100%;
62
70
  height: 100%;
63
71
  }
64
- `, A = b;
65
- function z(h) {
66
- return "adoptedStyleSheets" in h;
72
+ `, I = p;
73
+ function M(r) {
74
+ return "adoptedStyleSheets" in r;
67
75
  }
68
- function C(h) {
76
+ function z(r) {
69
77
  if (typeof CSSStyleSheet > "u") return null;
70
78
  try {
71
79
  const t = new CSSStyleSheet();
72
- return t.replaceSync(h), t;
80
+ return t.replaceSync(r), t;
73
81
  } catch {
74
82
  return null;
75
83
  }
76
84
  }
77
- const _ = C(b), u = {
85
+ const _ = z(p), u = {
78
86
  width: 300,
79
87
  height: 300,
80
88
  brushSrc: "",
@@ -99,8 +107,6 @@ class R {
99
107
  completing = !1;
100
108
  destroyed = !1;
101
109
  lastProgressEmitMs = 0;
102
- lastScratchX;
103
- lastScratchY;
104
110
  removeListeners;
105
111
  get canvas() {
106
112
  return this._canvas;
@@ -110,7 +116,7 @@ class R {
110
116
  throw new Error("ScratchReveal: container not found");
111
117
  this._canvas = this.createCanvas(this.config.width, this.config.height), this.ctx = this._canvas.getContext("2d", {
112
118
  willReadFrequently: !0
113
- }), this.brush = new M(this.ctx, 0, 0), this.brushSize = this.config.brushSize, this.container.appendChild(this._canvas);
119
+ }), this.brush = new C(this.ctx, 0, 0), this.brushSize = this.config.brushSize, this.container.appendChild(this._canvas);
114
120
  }
115
121
  async init() {
116
122
  if (!this.config.brushSrc)
@@ -144,25 +150,29 @@ class R {
144
150
  }
145
151
  bindEvents() {
146
152
  const e = E(
147
- (r, a) => {
153
+ (n, a) => {
148
154
  this.percent = this.updatePercent();
149
155
  const o = performance.now();
150
- (!this.lastProgressEmitMs || o - this.lastProgressEmitMs >= 120) && (this.lastProgressEmitMs = o, this.config.onProgress?.(this.percent)), this.finish(r, a);
156
+ (!this.lastProgressEmitMs || o - this.lastProgressEmitMs >= 120) && (this.lastProgressEmitMs = o, this.config.onProgress?.(this.percent)), this.finish(n, a);
151
157
  }
152
- ), s = E((r) => {
153
- this.updatePosition(r), this.scratch(), e(r, s);
154
- }), i = (r) => {
155
- r.preventDefault(), this.updatePosition(r), this.scratch(), this._canvas.setPointerCapture(r.pointerId), this._canvas.addEventListener("pointermove", s), this.resetScratchTrail(), this.percent = this.updatePercent(), this.config.onProgress?.(this.percent), this.finish(r, s);
156
- }, n = (r) => {
157
- this.resetScratchTrail(), this.percent = this.updatePercent(), this.config.onProgress?.(this.percent), this.finish(r, s);
158
+ ), s = E((n) => {
159
+ (n.buttons & 1) !== 0 && (this.updatePosition(n), this.scratch(), e(n, s));
160
+ }), i = (n) => {
161
+ try {
162
+ this._canvas.releasePointerCapture(n.pointerId);
163
+ } catch {
164
+ }
165
+ this._canvas.removeEventListener("pointermove", s), this.percent = this.updatePercent(), this.config.onProgress?.(this.percent), this.finish(n, s);
166
+ }, h = (n) => {
167
+ n.preventDefault(), this.updatePosition(n), this.scratch(), this._canvas.setPointerCapture(n.pointerId), this._canvas.addEventListener("pointermove", s), this.percent = this.updatePercent(), this.config.onProgress?.(this.percent), this.finish(n, s);
158
168
  };
159
- this._canvas.addEventListener("pointerdown", i), this._canvas.addEventListener("pointerup", n), this._canvas.addEventListener("pointerleave", n), this.removeListeners = () => {
160
- this._canvas.removeEventListener("pointerdown", i), this._canvas.removeEventListener("pointerup", n), this._canvas.removeEventListener("pointerleave", n), this._canvas.removeEventListener("pointermove", s);
169
+ this._canvas.addEventListener("pointerdown", h), this._canvas.addEventListener("pointerup", i), this._canvas.addEventListener("pointercancel", i), this.removeListeners = () => {
170
+ this._canvas.removeEventListener("pointerdown", h), this._canvas.removeEventListener("pointerup", i), this._canvas.removeEventListener("pointercancel", i), this._canvas.removeEventListener("pointermove", s);
161
171
  };
162
172
  }
163
173
  updatePosition(t) {
164
- const e = this._canvas.getBoundingClientRect(), s = e.width ? this._canvas.width / e.width : 1, i = e.height ? this._canvas.height / e.height : 1, n = (t.clientX - e.left) * s, r = (t.clientY - e.top) * i;
165
- this.brush.updateMousePosition(n, r);
174
+ const e = this._canvas.getBoundingClientRect(), s = e.width ? this._canvas.width / e.width : 1, i = e.height ? this._canvas.height / e.height : 1, h = (t.clientX - e.left) * s, n = (t.clientY - e.top) * i;
175
+ this.brush.updateMousePosition(h, n);
166
176
  }
167
177
  drawMask() {
168
178
  this.maskImage && (this.ctx.globalCompositeOperation = "source-over", this.ctx.clearRect(0, 0, this._canvas.width, this._canvas.height), this.ctx.drawImage(this.maskImage, 0, 0, this._canvas.width, this._canvas.height));
@@ -174,17 +184,8 @@ class R {
174
184
  }
175
185
  scratch() {
176
186
  if (!this.brushImage) return;
177
- const t = this.brushSize > 0 ? this.brushSize : Math.max(1, Math.min(this.brushImage.width, this.brushImage.height)), e = Math.max(2, t * 0.4);
178
- if (this.lastScratchX !== void 0 && this.lastScratchY !== void 0) {
179
- const i = this.brush.mouseX - this.lastScratchX, n = this.brush.mouseY - this.lastScratchY;
180
- if (i * i + n * n < e * e)
181
- return;
182
- }
183
- const s = this.brushSize > 0 ? this.brushSize : 0;
184
- this.ctx.globalCompositeOperation = "destination-out", this.ctx.save(), this.brush.brush(this.brushImage, s), this.ctx.restore(), this.lastScratchX = this.brush.mouseX, this.lastScratchY = this.brush.mouseY;
185
- }
186
- resetScratchTrail() {
187
- this.lastScratchX = void 0, this.lastScratchY = void 0;
187
+ const t = this.brushSize > 0 ? this.brushSize : 0;
188
+ this.ctx.globalCompositeOperation = "destination-out", this.ctx.save(), this.brush.brush(this.brushImage, t), this.ctx.restore();
188
189
  }
189
190
  updatePercent() {
190
191
  const e = this.ctx.getImageData(0, 0, this._canvas.width, this._canvas.height).data;
@@ -216,19 +217,19 @@ class R {
216
217
  this.ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
217
218
  }
218
219
  }
219
- function I(h = "scratch-reveal") {
220
- if (typeof window > "u" || !("customElements" in window) || customElements.get(h)) return;
221
- function t(s, i, n, r) {
222
- if (!s) return r;
220
+ function P(r = "scratch-reveal") {
221
+ if (typeof window > "u" || !("customElements" in window) || customElements.get(r)) return;
222
+ function t(s, i, h, n) {
223
+ if (!s) return n;
223
224
  const a = s.trim();
224
- if (!a) return r;
225
+ if (!a) return n;
225
226
  const o = Number.parseFloat(
226
227
  a.endsWith("%") ? a.slice(0, -1) : a.endsWith("px") ? a.slice(0, -2) : a
227
228
  );
228
- if (!Number.isFinite(o)) return r;
229
+ if (!Number.isFinite(o)) return n;
229
230
  if (a.endsWith("px"))
230
231
  return Math.max(0, o);
231
- const d = Math.min(i, n);
232
+ const d = Math.min(i, h);
232
233
  return Math.max(0, d * o / 100);
233
234
  }
234
235
  class e extends HTMLElement {
@@ -252,7 +253,7 @@ function I(h = "scratch-reveal") {
252
253
  constructor() {
253
254
  super();
254
255
  const i = this.attachShadow({ mode: "open" });
255
- z(i) && _ ? i.adoptedStyleSheets = [_] : (this.styleEl = document.createElement("style"), this.styleEl.textContent = b, i.append(this.styleEl)), this.container = document.createElement("div"), this.container.className = "sr", i.append(this.container);
256
+ M(i) && _ ? i.adoptedStyleSheets = [_] : (this.styleEl = document.createElement("style"), this.styleEl.textContent = p, i.append(this.styleEl)), this.container = document.createElement("div"), this.container.className = "sr", i.append(this.container);
256
257
  }
257
258
  connectedCallback() {
258
259
  this.scheduleRebuild();
@@ -260,8 +261,8 @@ function I(h = "scratch-reveal") {
260
261
  disconnectedCallback() {
261
262
  this.instance?.destroy(), this.instance = void 0, this.resizeObserver?.disconnect(), this.resizeObserver = void 0;
262
263
  }
263
- attributeChangedCallback(i, n, r) {
264
- n !== r && this.scheduleRebuild();
264
+ attributeChangedCallback(i, h, n) {
265
+ h !== n && this.scheduleRebuild();
265
266
  }
266
267
  scheduleRebuild() {
267
268
  this.rebuildScheduled || (this.rebuildScheduled = !0, queueMicrotask(() => {
@@ -270,27 +271,27 @@ function I(h = "scratch-reveal") {
270
271
  }
271
272
  rebuild() {
272
273
  this.container.replaceChildren(), this.instance?.destroy();
273
- const i = this.hasAttribute("width"), n = this.hasAttribute("height"), r = this.getBoundingClientRect(), a = Math.round(r.width), o = Math.round(r.height), d = i ? Number(this.getAttribute("width")) : a || u.width, m = n ? Number(this.getAttribute("height")) : o || u.height, y = Number(
274
+ const i = this.hasAttribute("width"), h = this.hasAttribute("height"), n = this.getBoundingClientRect(), a = Math.round(n.width), o = Math.round(n.height), d = i ? Number(this.getAttribute("width")) : a || u.width, m = h ? Number(this.getAttribute("height")) : o || u.height, y = Number(
274
275
  this.getAttribute("complete-percent") ?? u.percentToFinish
275
- ), p = (this.getAttribute("brush-src") ?? "").trim(), k = t(
276
+ ), b = (this.getAttribute("brush-src") ?? "").trim(), k = t(
276
277
  this.getAttribute("brush-size"),
277
278
  d,
278
279
  m,
279
280
  u.brushSize
280
- ), f = (this.getAttribute("mask-src") ?? "").trim(), S = (this.getAttribute("background-src") ?? "").trim(), l = [];
281
- if (p || l.push("brush-src"), f || l.push("mask-src"), S || l.push("background-src"), l.length) {
281
+ ), f = (this.getAttribute("mask-src") ?? "").trim(), w = (this.getAttribute("background-src") ?? "").trim(), l = [];
282
+ if (b || l.push("brush-src"), f || l.push("mask-src"), w || l.push("background-src"), l.length) {
282
283
  const c = `ScratchReveal: missing required attribute(s): ${l.join(", ")}`;
283
284
  this.renderError(c);
284
285
  return;
285
286
  }
286
- i ? this.container.style.width = `${d}px` : this.container.style.width = "100%", n ? this.container.style.height = `${m}px` : this.container.style.height = "100%", this.instance = new R(this.container, {
287
+ i ? this.container.style.width = `${d}px` : this.container.style.width = "100%", h ? this.container.style.height = `${m}px` : this.container.style.height = "100%", this.instance = new R(this.container, {
287
288
  width: d,
288
289
  height: m,
289
290
  percentToFinish: y,
290
- brushSrc: p,
291
+ brushSrc: b,
291
292
  brushSize: k,
292
293
  imageMaskSrc: f,
293
- imageBackgroundSrc: S,
294
+ imageBackgroundSrc: w,
294
295
  onProgress: (c) => {
295
296
  this.dispatchEvent(new CustomEvent("progress", { detail: { percent: c } }));
296
297
  },
@@ -300,17 +301,17 @@ function I(h = "scratch-reveal") {
300
301
  }), this.instance.init().catch((c) => {
301
302
  const g = c instanceof Error ? c.message : "ScratchReveal: init failed";
302
303
  this.renderError(g);
303
- }), (!i || !n) && "ResizeObserver" in window ? (this.resizeObserver?.disconnect(), this.resizeObserver = new ResizeObserver(() => {
304
+ }), (!i || !h) && "ResizeObserver" in window ? (this.resizeObserver?.disconnect(), this.resizeObserver = new ResizeObserver(() => {
304
305
  if (this.hasAttribute("width") && this.hasAttribute("height")) {
305
306
  this.resizeObserver?.disconnect(), this.resizeObserver = void 0;
306
307
  return;
307
308
  }
308
- const c = this.getBoundingClientRect(), g = Math.round(c.width), w = Math.round(c.height);
309
- this.instance?.resize(g, w);
309
+ const c = this.getBoundingClientRect(), g = Math.round(c.width), S = Math.round(c.height);
310
+ this.instance?.resize(g, S);
310
311
  const x = t(
311
312
  this.getAttribute("brush-size"),
312
313
  g,
313
- w,
314
+ S,
314
315
  u.brushSize
315
316
  );
316
317
  this.instance?.setBrushSize(x);
@@ -320,13 +321,13 @@ function I(h = "scratch-reveal") {
320
321
  this.instance?.destroy(), this.instance = void 0, this.lastErrorMessage !== i && (this.lastErrorMessage = i, this.dispatchEvent(new CustomEvent("error", { detail: { message: i } })));
321
322
  }
322
323
  }
323
- customElements.define(h, e);
324
+ customElements.define(r, e);
324
325
  }
325
- function T(h) {
326
- I(), h.config.globalProperties.$scratchReveal = !0;
326
+ function T(r) {
327
+ P(), r.config.globalProperties.$scratchReveal = !0;
327
328
  }
328
329
  export {
329
330
  T as installScratchReveal,
330
- I as registerScratchRevealElement,
331
- A as scratchRevealCssText
331
+ P as registerScratchRevealElement,
332
+ I as scratchRevealCssText
332
333
  };
package/dist/index.umd.js CHANGED
@@ -1,4 +1,4 @@
1
- (function(o,l){typeof exports=="object"&&typeof module<"u"?l(exports):typeof define=="function"&&define.amd?define(["exports"],l):(o=typeof globalThis<"u"?globalThis:o||self,l(o.ScratchReveal={}))})(this,(function(o){"use strict";class l{ctx;mouseX;mouseY;constructor(t,e,s){this.ctx=t,this.mouseX=e,this.mouseY=s}updateMousePosition(t,e){this.mouseX=t,this.mouseY=e}brush(t,e=0){if(!t)return;const s=Math.atan2(this.mouseY,this.mouseX);if(this.ctx.save(),this.ctx.translate(this.mouseX,this.mouseY),this.ctx.rotate(s),e>0){const i=e,n=e*(t.height/t.width);this.ctx.drawImage(t,-(i/2),-(n/2),i,n)}else this.ctx.drawImage(t,-(t.width/2),-(t.height/2));this.ctx.restore()}}function f(h){return new Promise((t,e)=>{const s=new Image;s.crossOrigin="anonymous",s.onload=()=>t(s),s.onerror=()=>e(new Error(`Image ${h} failed to load`)),s.src=h})}function S(h){let t=0;return((...s)=>{t||(t=requestAnimationFrame(()=>{t=0,h(...s)}))})}const b=`.sr {
1
+ (function(o,l){typeof exports=="object"&&typeof module<"u"?l(exports):typeof define=="function"&&define.amd?define(["exports"],l):(o=typeof globalThis<"u"?globalThis:o||self,l(o.ScratchReveal={}))})(this,(function(o){"use strict";class l{ctx;mouseX;mouseY;constructor(t,e,s){this.ctx=t,this.mouseX=e,this.mouseY=s}updateMousePosition(t,e){this.mouseX=t,this.mouseY=e}brush(t,e=0){if(!t)return;const s=Math.atan2(this.mouseY,this.mouseX);if(this.ctx.save(),this.ctx.translate(this.mouseX,this.mouseY),this.ctx.rotate(s),e>0){const i=e,h=e*(t.height/t.width);this.ctx.drawImage(t,-(i/2),-(h/2),i,h)}else this.ctx.drawImage(t,-(t.width/2),-(t.height/2));this.ctx.restore()}}function f(r){return new Promise((t,e)=>{const s=new Image;s.crossOrigin="anonymous",s.onload=()=>t(s),s.onerror=()=>e(new Error(`Image ${r} failed to load`)),s.src=r})}function S(r){let t=0;return((...s)=>{t||(t=requestAnimationFrame(()=>{t=0,r(...s)}))})}const p=`.sr {
2
2
  position: relative;
3
3
  overflow: hidden;
4
4
  width: 100%;
@@ -11,6 +11,10 @@
11
11
  width: 100%;
12
12
  height: 100%;
13
13
  object-fit: cover;
14
+ user-select: none;
15
+ -webkit-user-select: none;
16
+ -webkit-user-drag: none;
17
+ pointer-events: none;
14
18
  }
15
19
 
16
20
  .sr__canvas {
@@ -18,6 +22,10 @@
18
22
  inset: 0;
19
23
  width: 100%;
20
24
  height: 100%;
25
+ touch-action: none;
26
+ user-select: none;
27
+ -webkit-user-select: none;
28
+ -webkit-tap-highlight-color: transparent;
21
29
  }
22
30
 
23
31
  scratch-reveal {
@@ -25,4 +33,4 @@ scratch-reveal {
25
33
  width: 100%;
26
34
  height: 100%;
27
35
  }
28
- `,M=b;function R(h){return"adoptedStyleSheets"in h}function z(h){if(typeof CSSStyleSheet>"u")return null;try{const t=new CSSStyleSheet;return t.replaceSync(h),t}catch{return null}}const w=z(b),d={width:300,height:300,brushSrc:"",imageMaskSrc:"",imageBackgroundSrc:"",brushSize:0,percentToFinish:60};class C{config;ctx;container;_canvas;brush;maskImage;backgroundImage;brushImage;backgroundEl;brushSize=0;percent=0;done=!1;completing=!1;destroyed=!1;lastProgressEmitMs=0;lastScratchX;lastScratchY;removeListeners;get canvas(){return this._canvas}constructor(t,e={}){if(this.config={...d,...e},this.container=typeof t=="string"?document.querySelector(t):t,!this.container)throw new Error("ScratchReveal: container not found");this._canvas=this.createCanvas(this.config.width,this.config.height),this.ctx=this._canvas.getContext("2d",{willReadFrequently:!0}),this.brush=new l(this.ctx,0,0),this.brushSize=this.config.brushSize,this.container.appendChild(this._canvas)}async init(){if(!this.config.brushSrc)throw new Error('ScratchReveal: "brushSrc" is required');if(!this.config.imageMaskSrc)throw new Error('ScratchReveal: "imageMaskSrc" is required');if(!this.config.imageBackgroundSrc)throw new Error('ScratchReveal: "imageBackgroundSrc" is required');const[t,e,s]=await Promise.all([f(this.config.brushSrc),f(this.config.imageMaskSrc),f(this.config.imageBackgroundSrc)]);return this.destroyed?this:(this.brushImage=t,this.maskImage=e,this.backgroundImage=s,this.drawMask(),this.setBackground(),this.bindEvents(),this)}destroy(){this.destroyed=!0,this.removeListeners?.()}getPercent(){return this.percent}createCanvas(t,e){const s=document.createElement("canvas");return s.className="sr__canvas",s.width=t,s.height=e,s.style.width="100%",s.style.height="100%",s}resize(t,e){this.destroyed||this.done||t<=0||e<=0||this._canvas.width===t&&this._canvas.height===e||(this._canvas.width=t,this._canvas.height=e,this.percent=0,this.ctx.globalCompositeOperation="source-over",this.drawMask())}setBrushSize(t){this.destroyed||!Number.isFinite(t)||t<0||(this.brushSize=t)}bindEvents(){const e=S((r,a)=>{this.percent=this.updatePercent();const u=performance.now();(!this.lastProgressEmitMs||u-this.lastProgressEmitMs>=120)&&(this.lastProgressEmitMs=u,this.config.onProgress?.(this.percent)),this.finish(r,a)}),s=S(r=>{this.updatePosition(r),this.scratch(),e(r,s)}),i=r=>{r.preventDefault(),this.updatePosition(r),this.scratch(),this._canvas.setPointerCapture(r.pointerId),this._canvas.addEventListener("pointermove",s),this.resetScratchTrail(),this.percent=this.updatePercent(),this.config.onProgress?.(this.percent),this.finish(r,s)},n=r=>{this.resetScratchTrail(),this.percent=this.updatePercent(),this.config.onProgress?.(this.percent),this.finish(r,s)};this._canvas.addEventListener("pointerdown",i),this._canvas.addEventListener("pointerup",n),this._canvas.addEventListener("pointerleave",n),this.removeListeners=()=>{this._canvas.removeEventListener("pointerdown",i),this._canvas.removeEventListener("pointerup",n),this._canvas.removeEventListener("pointerleave",n),this._canvas.removeEventListener("pointermove",s)}}updatePosition(t){const e=this._canvas.getBoundingClientRect(),s=e.width?this._canvas.width/e.width:1,i=e.height?this._canvas.height/e.height:1,n=(t.clientX-e.left)*s,r=(t.clientY-e.top)*i;this.brush.updateMousePosition(n,r)}drawMask(){this.maskImage&&(this.ctx.globalCompositeOperation="source-over",this.ctx.clearRect(0,0,this._canvas.width,this._canvas.height),this.ctx.drawImage(this.maskImage,0,0,this._canvas.width,this._canvas.height))}setBackground(){if(this.destroyed||!this.backgroundImage||!this.container.contains(this._canvas))return;const t=this.backgroundEl??document.createElement("img");t.src=this.backgroundImage.src,t.className="sr__bg",t.isConnected||this.container.insertBefore(t,this._canvas),this.backgroundEl=t}scratch(){if(!this.brushImage)return;const t=this.brushSize>0?this.brushSize:Math.max(1,Math.min(this.brushImage.width,this.brushImage.height)),e=Math.max(2,t*.4);if(this.lastScratchX!==void 0&&this.lastScratchY!==void 0){const i=this.brush.mouseX-this.lastScratchX,n=this.brush.mouseY-this.lastScratchY;if(i*i+n*n<e*e)return}const s=this.brushSize>0?this.brushSize:0;this.ctx.globalCompositeOperation="destination-out",this.ctx.save(),this.brush.brush(this.brushImage,s),this.ctx.restore(),this.lastScratchX=this.brush.mouseX,this.lastScratchY=this.brush.mouseY}resetScratchTrail(){this.lastScratchX=void 0,this.lastScratchY=void 0}updatePercent(){const e=this.ctx.getImageData(0,0,this._canvas.width,this._canvas.height).data;let s=0;for(let i=3;i<e.length;i+=4)e[i]===0&&s++;return s/(this._canvas.width*this._canvas.height)*100}finish(t,e){if(!this.done&&this.percent>this.config.percentToFinish&&(this.done=!0,this._canvas.style.pointerEvents="none",this.config.onComplete?.(),this.playCompleteEffect(),t&&e)){try{this._canvas.releasePointerCapture(t.pointerId)}catch{}this._canvas.removeEventListener("pointermove",e)}}playCompleteEffect(){if(this.destroyed||this.completing)return;this.completing=!0;const t=350,e="ease-out";this._canvas.style.transition=`opacity ${t}ms ${e}`,this._canvas.style.opacity||(this._canvas.style.opacity="1"),requestAnimationFrame(()=>{this._canvas.style.opacity="0",window.setTimeout(()=>{this.destroyed||this.clear()},t)})}clear(){this.ctx.clearRect(0,0,this._canvas.width,this._canvas.height)}}function E(h="scratch-reveal"){if(typeof window>"u"||!("customElements"in window)||customElements.get(h))return;function t(s,i,n,r){if(!s)return r;const a=s.trim();if(!a)return r;const u=Number.parseFloat(a.endsWith("%")?a.slice(0,-1):a.endsWith("px")?a.slice(0,-2):a);if(!Number.isFinite(u))return r;if(a.endsWith("px"))return Math.max(0,u);const g=Math.min(i,n);return Math.max(0,g*u/100)}class e extends HTMLElement{instance;container;styleEl=null;rebuildScheduled=!1;resizeObserver;lastErrorMessage=null;static get observedAttributes(){return["width","height","complete-percent","brush-src","brush-size","mask-src","background-src"]}constructor(){super();const i=this.attachShadow({mode:"open"});R(i)&&w?i.adoptedStyleSheets=[w]:(this.styleEl=document.createElement("style"),this.styleEl.textContent=b,i.append(this.styleEl)),this.container=document.createElement("div"),this.container.className="sr",i.append(this.container)}connectedCallback(){this.scheduleRebuild()}disconnectedCallback(){this.instance?.destroy(),this.instance=void 0,this.resizeObserver?.disconnect(),this.resizeObserver=void 0}attributeChangedCallback(i,n,r){n!==r&&this.scheduleRebuild()}scheduleRebuild(){this.rebuildScheduled||(this.rebuildScheduled=!0,queueMicrotask(()=>{this.rebuildScheduled=!1,this.isConnected&&this.rebuild()}))}rebuild(){this.container.replaceChildren(),this.instance?.destroy();const i=this.hasAttribute("width"),n=this.hasAttribute("height"),r=this.getBoundingClientRect(),a=Math.round(r.width),u=Math.round(r.height),g=i?Number(this.getAttribute("width")):a||d.width,p=n?Number(this.getAttribute("height")):u||d.height,T=Number(this.getAttribute("complete-percent")??d.percentToFinish),y=(this.getAttribute("brush-src")??"").trim(),I=t(this.getAttribute("brush-size"),g,p,d.brushSize),_=(this.getAttribute("mask-src")??"").trim(),k=(this.getAttribute("background-src")??"").trim(),m=[];if(y||m.push("brush-src"),_||m.push("mask-src"),k||m.push("background-src"),m.length){const c=`ScratchReveal: missing required attribute(s): ${m.join(", ")}`;this.renderError(c);return}i?this.container.style.width=`${g}px`:this.container.style.width="100%",n?this.container.style.height=`${p}px`:this.container.style.height="100%",this.instance=new C(this.container,{width:g,height:p,percentToFinish:T,brushSrc:y,brushSize:I,imageMaskSrc:_,imageBackgroundSrc:k,onProgress:c=>{this.dispatchEvent(new CustomEvent("progress",{detail:{percent:c}}))},onComplete:()=>{this.dispatchEvent(new CustomEvent("complete",{detail:{percent:100}}))}}),this.instance.init().catch(c=>{const v=c instanceof Error?c.message:"ScratchReveal: init failed";this.renderError(v)}),(!i||!n)&&"ResizeObserver"in window?(this.resizeObserver?.disconnect(),this.resizeObserver=new ResizeObserver(()=>{if(this.hasAttribute("width")&&this.hasAttribute("height")){this.resizeObserver?.disconnect(),this.resizeObserver=void 0;return}const c=this.getBoundingClientRect(),v=Math.round(c.width),x=Math.round(c.height);this.instance?.resize(v,x);const A=t(this.getAttribute("brush-size"),v,x,d.brushSize);this.instance?.setBrushSize(A)}),this.resizeObserver.observe(this)):(this.resizeObserver?.disconnect(),this.resizeObserver=void 0)}renderError(i){this.instance?.destroy(),this.instance=void 0,this.lastErrorMessage!==i&&(this.lastErrorMessage=i,this.dispatchEvent(new CustomEvent("error",{detail:{message:i}})))}}customElements.define(h,e)}function P(h){E(),h.config.globalProperties.$scratchReveal=!0}o.installScratchReveal=P,o.registerScratchRevealElement=E,o.scratchRevealCssText=M,Object.defineProperty(o,Symbol.toStringTag,{value:"Module"})}));
36
+ `,C=p;function R(r){return"adoptedStyleSheets"in r}function M(r){if(typeof CSSStyleSheet>"u")return null;try{const t=new CSSStyleSheet;return t.replaceSync(r),t}catch{return null}}const w=M(p),d={width:300,height:300,brushSrc:"",imageMaskSrc:"",imageBackgroundSrc:"",brushSize:0,percentToFinish:60};class z{config;ctx;container;_canvas;brush;maskImage;backgroundImage;brushImage;backgroundEl;brushSize=0;percent=0;done=!1;completing=!1;destroyed=!1;lastProgressEmitMs=0;removeListeners;get canvas(){return this._canvas}constructor(t,e={}){if(this.config={...d,...e},this.container=typeof t=="string"?document.querySelector(t):t,!this.container)throw new Error("ScratchReveal: container not found");this._canvas=this.createCanvas(this.config.width,this.config.height),this.ctx=this._canvas.getContext("2d",{willReadFrequently:!0}),this.brush=new l(this.ctx,0,0),this.brushSize=this.config.brushSize,this.container.appendChild(this._canvas)}async init(){if(!this.config.brushSrc)throw new Error('ScratchReveal: "brushSrc" is required');if(!this.config.imageMaskSrc)throw new Error('ScratchReveal: "imageMaskSrc" is required');if(!this.config.imageBackgroundSrc)throw new Error('ScratchReveal: "imageBackgroundSrc" is required');const[t,e,s]=await Promise.all([f(this.config.brushSrc),f(this.config.imageMaskSrc),f(this.config.imageBackgroundSrc)]);return this.destroyed?this:(this.brushImage=t,this.maskImage=e,this.backgroundImage=s,this.drawMask(),this.setBackground(),this.bindEvents(),this)}destroy(){this.destroyed=!0,this.removeListeners?.()}getPercent(){return this.percent}createCanvas(t,e){const s=document.createElement("canvas");return s.className="sr__canvas",s.width=t,s.height=e,s.style.width="100%",s.style.height="100%",s}resize(t,e){this.destroyed||this.done||t<=0||e<=0||this._canvas.width===t&&this._canvas.height===e||(this._canvas.width=t,this._canvas.height=e,this.percent=0,this.ctx.globalCompositeOperation="source-over",this.drawMask())}setBrushSize(t){this.destroyed||!Number.isFinite(t)||t<0||(this.brushSize=t)}bindEvents(){const e=S((n,a)=>{this.percent=this.updatePercent();const u=performance.now();(!this.lastProgressEmitMs||u-this.lastProgressEmitMs>=120)&&(this.lastProgressEmitMs=u,this.config.onProgress?.(this.percent)),this.finish(n,a)}),s=S(n=>{(n.buttons&1)!==0&&(this.updatePosition(n),this.scratch(),e(n,s))}),i=n=>{try{this._canvas.releasePointerCapture(n.pointerId)}catch{}this._canvas.removeEventListener("pointermove",s),this.percent=this.updatePercent(),this.config.onProgress?.(this.percent),this.finish(n,s)},h=n=>{n.preventDefault(),this.updatePosition(n),this.scratch(),this._canvas.setPointerCapture(n.pointerId),this._canvas.addEventListener("pointermove",s),this.percent=this.updatePercent(),this.config.onProgress?.(this.percent),this.finish(n,s)};this._canvas.addEventListener("pointerdown",h),this._canvas.addEventListener("pointerup",i),this._canvas.addEventListener("pointercancel",i),this.removeListeners=()=>{this._canvas.removeEventListener("pointerdown",h),this._canvas.removeEventListener("pointerup",i),this._canvas.removeEventListener("pointercancel",i),this._canvas.removeEventListener("pointermove",s)}}updatePosition(t){const e=this._canvas.getBoundingClientRect(),s=e.width?this._canvas.width/e.width:1,i=e.height?this._canvas.height/e.height:1,h=(t.clientX-e.left)*s,n=(t.clientY-e.top)*i;this.brush.updateMousePosition(h,n)}drawMask(){this.maskImage&&(this.ctx.globalCompositeOperation="source-over",this.ctx.clearRect(0,0,this._canvas.width,this._canvas.height),this.ctx.drawImage(this.maskImage,0,0,this._canvas.width,this._canvas.height))}setBackground(){if(this.destroyed||!this.backgroundImage||!this.container.contains(this._canvas))return;const t=this.backgroundEl??document.createElement("img");t.src=this.backgroundImage.src,t.className="sr__bg",t.isConnected||this.container.insertBefore(t,this._canvas),this.backgroundEl=t}scratch(){if(!this.brushImage)return;const t=this.brushSize>0?this.brushSize:0;this.ctx.globalCompositeOperation="destination-out",this.ctx.save(),this.brush.brush(this.brushImage,t),this.ctx.restore()}updatePercent(){const e=this.ctx.getImageData(0,0,this._canvas.width,this._canvas.height).data;let s=0;for(let i=3;i<e.length;i+=4)e[i]===0&&s++;return s/(this._canvas.width*this._canvas.height)*100}finish(t,e){if(!this.done&&this.percent>this.config.percentToFinish&&(this.done=!0,this._canvas.style.pointerEvents="none",this.config.onComplete?.(),this.playCompleteEffect(),t&&e)){try{this._canvas.releasePointerCapture(t.pointerId)}catch{}this._canvas.removeEventListener("pointermove",e)}}playCompleteEffect(){if(this.destroyed||this.completing)return;this.completing=!0;const t=350,e="ease-out";this._canvas.style.transition=`opacity ${t}ms ${e}`,this._canvas.style.opacity||(this._canvas.style.opacity="1"),requestAnimationFrame(()=>{this._canvas.style.opacity="0",window.setTimeout(()=>{this.destroyed||this.clear()},t)})}clear(){this.ctx.clearRect(0,0,this._canvas.width,this._canvas.height)}}function E(r="scratch-reveal"){if(typeof window>"u"||!("customElements"in window)||customElements.get(r))return;function t(s,i,h,n){if(!s)return n;const a=s.trim();if(!a)return n;const u=Number.parseFloat(a.endsWith("%")?a.slice(0,-1):a.endsWith("px")?a.slice(0,-2):a);if(!Number.isFinite(u))return n;if(a.endsWith("px"))return Math.max(0,u);const g=Math.min(i,h);return Math.max(0,g*u/100)}class e extends HTMLElement{instance;container;styleEl=null;rebuildScheduled=!1;resizeObserver;lastErrorMessage=null;static get observedAttributes(){return["width","height","complete-percent","brush-src","brush-size","mask-src","background-src"]}constructor(){super();const i=this.attachShadow({mode:"open"});R(i)&&w?i.adoptedStyleSheets=[w]:(this.styleEl=document.createElement("style"),this.styleEl.textContent=p,i.append(this.styleEl)),this.container=document.createElement("div"),this.container.className="sr",i.append(this.container)}connectedCallback(){this.scheduleRebuild()}disconnectedCallback(){this.instance?.destroy(),this.instance=void 0,this.resizeObserver?.disconnect(),this.resizeObserver=void 0}attributeChangedCallback(i,h,n){h!==n&&this.scheduleRebuild()}scheduleRebuild(){this.rebuildScheduled||(this.rebuildScheduled=!0,queueMicrotask(()=>{this.rebuildScheduled=!1,this.isConnected&&this.rebuild()}))}rebuild(){this.container.replaceChildren(),this.instance?.destroy();const i=this.hasAttribute("width"),h=this.hasAttribute("height"),n=this.getBoundingClientRect(),a=Math.round(n.width),u=Math.round(n.height),g=i?Number(this.getAttribute("width")):a||d.width,b=h?Number(this.getAttribute("height")):u||d.height,A=Number(this.getAttribute("complete-percent")??d.percentToFinish),_=(this.getAttribute("brush-src")??"").trim(),I=t(this.getAttribute("brush-size"),g,b,d.brushSize),y=(this.getAttribute("mask-src")??"").trim(),k=(this.getAttribute("background-src")??"").trim(),m=[];if(_||m.push("brush-src"),y||m.push("mask-src"),k||m.push("background-src"),m.length){const c=`ScratchReveal: missing required attribute(s): ${m.join(", ")}`;this.renderError(c);return}i?this.container.style.width=`${g}px`:this.container.style.width="100%",h?this.container.style.height=`${b}px`:this.container.style.height="100%",this.instance=new z(this.container,{width:g,height:b,percentToFinish:A,brushSrc:_,brushSize:I,imageMaskSrc:y,imageBackgroundSrc:k,onProgress:c=>{this.dispatchEvent(new CustomEvent("progress",{detail:{percent:c}}))},onComplete:()=>{this.dispatchEvent(new CustomEvent("complete",{detail:{percent:100}}))}}),this.instance.init().catch(c=>{const v=c instanceof Error?c.message:"ScratchReveal: init failed";this.renderError(v)}),(!i||!h)&&"ResizeObserver"in window?(this.resizeObserver?.disconnect(),this.resizeObserver=new ResizeObserver(()=>{if(this.hasAttribute("width")&&this.hasAttribute("height")){this.resizeObserver?.disconnect(),this.resizeObserver=void 0;return}const c=this.getBoundingClientRect(),v=Math.round(c.width),x=Math.round(c.height);this.instance?.resize(v,x);const T=t(this.getAttribute("brush-size"),v,x,d.brushSize);this.instance?.setBrushSize(T)}),this.resizeObserver.observe(this)):(this.resizeObserver?.disconnect(),this.resizeObserver=void 0)}renderError(i){this.instance?.destroy(),this.instance=void 0,this.lastErrorMessage!==i&&(this.lastErrorMessage=i,this.dispatchEvent(new CustomEvent("error",{detail:{message:i}})))}}customElements.define(r,e)}function P(r){E(),r.config.globalProperties.$scratchReveal=!0}o.installScratchReveal=P,o.registerScratchRevealElement=E,o.scratchRevealCssText=C,Object.defineProperty(o,Symbol.toStringTag,{value:"Module"})}));
@@ -1 +1 @@
1
- .sr{width:100%;height:100%;position:relative;overflow:hidden}.sr__bg{object-fit:cover;width:100%;height:100%;display:block;position:relative}.sr__canvas{width:100%;height:100%;position:absolute;inset:0}scratch-reveal{width:100%;height:100%;display:inline-block}
1
+ .sr{width:100%;height:100%;position:relative;overflow:hidden}.sr__bg{object-fit:cover;-webkit-user-select:none;user-select:none;-webkit-user-drag:none;pointer-events:none;width:100%;height:100%;display:block;position:relative}.sr__canvas{touch-action:none;-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;width:100%;height:100%;position:absolute;inset:0}scratch-reveal{width:100%;height:100%;display:inline-block}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scratch-reveal",
3
- "version": "1.1.0",
3
+ "version": "1.2.1-dev.0",
4
4
  "description": "Scratch & reveal Web Component: mask + background with brush-only reveal, Vue-friendly, shadow-styled.",
5
5
  "author": "ux-ui.pro",
6
6
  "license": "MIT",