scratch-reveal 1.0.0-dev.0 → 1.0.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 CHANGED
@@ -60,6 +60,7 @@ app.mount('#app');
60
60
  </div>
61
61
  ```
62
62
  — If `width`/`height` attributes are omitted, the component will observe its own size and resize the canvas accordingly.
63
+
63
64
  <br>
64
65
 
65
66
  ➠ **Events**
@@ -74,6 +75,7 @@ el.addEventListener('complete', () => {
74
75
  ```
75
76
  — `progress` (detail: `{ percent: number }`)
76
77
  — `complete` (detail: `{ percent: 100 }`)
78
+
77
79
  <br>
78
80
 
79
81
  ➠ **Attributes**
package/dist/index.cjs.js CHANGED
@@ -1,4 +1,4 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});class P{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){const i=new Error("Brush.brush: img is required");console.log(i.message);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,r=e*(t.height/t.width);this.ctx.drawImage(t,-(i/2),-(r/2),i,r)}else this.ctx.drawImage(t,-(t.width/2),-(t.height/2));this.ctx.restore()}}function b(n){return new Promise((t,e)=>{const s=new Image;s.crossOrigin="anonymous",s.onload=()=>t(s),s.onerror=()=>e(new Error(`Image ${n} failed to load`)),s.src=n})}function A(n){let t=0;return((...s)=>{t||(t=requestAnimationFrame(()=>{t=0,n(...s)}))})}function m(n){const t=n.getBoundingClientRect();return{left:t.left+window.scrollX,top:t.top+window.scrollY}}const v=`.sr {
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){const i=new Error("Brush.brush: img is required");console.log(i.message);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 b(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 P(h){let t=0;return((...s)=>{t||(t=requestAnimationFrame(()=>{t=0,h(...s)}))})}const m=`.sr {
2
2
  position: relative;
3
3
  overflow: hidden;
4
4
  width: 100%;
@@ -22,7 +22,7 @@
22
22
 
23
23
  scratch-reveal {
24
24
  display: inline-block;
25
- width: 300px;
26
- height: 300px;
25
+ width: 100%;
26
+ height: 100%;
27
27
  }
28
- `,R=v;function I(n){return"adoptedStyleSheets"in n}function M(n){if(typeof CSSStyleSheet>"u")return null;try{const t=new CSSStyleSheet;return t.replaceSync(n),t}catch{return null}}const w=M(v),a={width:300,height:300,brushSrc:"/demo/assets/brush.png",imageMaskSrc:"/demo/assets/scratch-reveal.png",imageBackgroundSrc:"/demo/assets/scratch-reveal-background.svg",brushSize:0,percentToFinish:60,enabledPercentUpdate:!0};class L{config;ctx;container;_canvas;brush;maskImage;backgroundImage;brushImage;backgroundEl;brushSize=0;percent=0;done=!1;destroyed=!1;zone={top:0,left:0};removeListeners;get canvas(){return this._canvas}constructor(t,e={}){if(this.config={...a,...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 P(this.ctx,0,0),this.brushSize=this.config.brushSize,this.container.appendChild(this._canvas)}async init(){const[t,e,s]=await Promise.all([b(this.config.brushSrc),b(this.config.imageMaskSrc),b(this.config.imageBackgroundSrc)]);return this.destroyed?this:(this.brushImage=t,this.maskImage=e,this.backgroundImage=s,this.drawMask(),this.setBackground(),this.zone=m(this._canvas),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(),this.zone=m(this._canvas))}setBrushSize(t){this.destroyed||!Number.isFinite(t)||t<0||(this.brushSize=t)}bindEvents(){const t=A(i=>{this.updatePosition(i),this.scratch(),this.config.enabledPercentUpdate&&(this.percent=this.updatePercent(),this.config.onProgress?.(this.percent)),this.finish(i,t)}),e=i=>{i.preventDefault(),this.zone=m(this._canvas),this.updatePosition(i),this.scratch(),this._canvas.setPointerCapture(i.pointerId),this._canvas.addEventListener("pointermove",t),this.config.enabledPercentUpdate&&(this.percent=this.updatePercent(),this.config.onProgress?.(this.percent)),this.finish(i,t)},s=i=>{this.finish(i,t)};this._canvas.addEventListener("pointerdown",e),this._canvas.addEventListener("pointerup",s),this._canvas.addEventListener("pointerleave",s),this.removeListeners=()=>{this._canvas.removeEventListener("pointerdown",e),this._canvas.removeEventListener("pointerup",s),this._canvas.removeEventListener("pointerleave",s),this._canvas.removeEventListener("pointermove",t)}}updatePosition(t){const e=t.clientX-this.zone.left,s=t.clientY-this.zone.top;this.brush.updateMousePosition(e,s)}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(){this.brushImage&&(this.ctx.globalCompositeOperation="destination-out",this.ctx.save(),this.brush.brush(this.brushImage,this.brushSize),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.clear(),this._canvas.style.pointerEvents="none",this.config.onComplete?.(),t&&e)){try{this._canvas.releasePointerCapture(t.pointerId)}catch{}this._canvas.removeEventListener("pointermove",e)}}clear(){this.ctx.clearRect(0,0,this._canvas.width,this._canvas.height)}}function S(n="scratch-reveal"){if(typeof window>"u"||!("customElements"in window)||customElements.get(n))return;function t(i,r){if(i===null)return r;const h=i.trim().toLowerCase();return h===""?!0:h==="false"||h==="0"||h==="no"||h==="off"?!1:h==="true"||h==="1"||h==="yes"||h==="on"?!0:r}function e(i,r,h,c){if(!i)return c;const o=i.trim();if(!o)return c;if(o.endsWith("%")){const u=Number.parseFloat(o.slice(0,-1));if(!Number.isFinite(u))return c;const d=Math.min(r,h);return Math.max(0,d*u/100)}const l=o.endsWith("px")?Number.parseFloat(o.slice(0,-2)):Number.parseFloat(o);return Number.isFinite(l)?Math.max(0,l):c}class s extends HTMLElement{instance;container;styleEl=null;rebuildScheduled=!1;resizeObserver;static get observedAttributes(){return["width","height","percent-to-finish","brush-src","brush-size","mask-src","background-src","enabled-percent-update"]}constructor(){super();const r=this.attachShadow({mode:"open"});I(r)&&w?r.adoptedStyleSheets=[w]:(this.styleEl=document.createElement("style"),this.styleEl.textContent=v,r.append(this.styleEl)),this.container=document.createElement("div"),this.container.className="sr",r.append(this.container)}connectedCallback(){this.scheduleRebuild()}disconnectedCallback(){this.instance?.destroy(),this.instance=void 0,this.resizeObserver?.disconnect(),this.resizeObserver=void 0}attributeChangedCallback(r,h,c){h!==c&&this.scheduleRebuild()}scheduleRebuild(){this.rebuildScheduled||(this.rebuildScheduled=!0,queueMicrotask(()=>{this.rebuildScheduled=!1,this.isConnected&&this.rebuild()}))}rebuild(){this.container.replaceChildren(),this.instance?.destroy();const r=this.hasAttribute("width"),h=this.hasAttribute("height"),c=this.getBoundingClientRect(),o=Math.round(c.width),l=Math.round(c.height),u=r?Number(this.getAttribute("width")):o||a.width,d=h?Number(this.getAttribute("height")):l||a.height,_=Number(this.getAttribute("percent-to-finish")??a.percentToFinish),E=this.getAttribute("brush-src")??a.brushSrc,y=t(this.getAttribute("enabled-percent-update"),a.enabledPercentUpdate),x=e(this.getAttribute("brush-size"),u,d,a.brushSize),k=this.getAttribute("mask-src")??a.imageMaskSrc,z=this.getAttribute("background-src")??a.imageBackgroundSrc;r?this.container.style.width=`${u}px`:this.container.style.width="100%",h?this.container.style.height=`${d}px`:this.container.style.height="100%",this.instance=new L(this.container,{width:u,height:d,percentToFinish:_,brushSrc:E,brushSize:x,imageMaskSrc:k,imageBackgroundSrc:z,enabledPercentUpdate:y,onProgress:g=>{this.dispatchEvent(new CustomEvent("progress",{detail:{percent:g}}))},onComplete:()=>{this.dispatchEvent(new CustomEvent("complete",{detail:{percent:100}}))}}),this.instance.init(),(!r||!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 g=this.getBoundingClientRect(),p=Math.round(g.width),f=Math.round(g.height);this.instance?.resize(p,f);const C=e(this.getAttribute("brush-size"),p,f,a.brushSize);this.instance?.setBrushSize(C)}),this.resizeObserver.observe(this)):(this.resizeObserver?.disconnect(),this.resizeObserver=void 0)}}customElements.define(n,s)}function O(n){S(),n.config.globalProperties.$scratchReveal=!0}exports.installScratchReveal=O;exports.registerScratchRevealElement=S;exports.scratchRevealCssText=R;
28
+ `,A=m;function R(h){return"adoptedStyleSheets"in h}function I(h){if(typeof CSSStyleSheet>"u")return null;try{const t=new CSSStyleSheet;return t.replaceSync(h),t}catch{return null}}const f=I(m),a={width:300,height:300,brushSrc:"/demo/assets/brush.png",imageMaskSrc:"/demo/assets/scratch-reveal.png",imageBackgroundSrc:"/demo/assets/scratch-reveal-background.svg",brushSize:0,percentToFinish:60,enabledPercentUpdate:!0};class M{config;ctx;container;_canvas;brush;maskImage;backgroundImage;brushImage;backgroundEl;brushSize=0;percent=0;done=!1;destroyed=!1;removeListeners;get canvas(){return this._canvas}constructor(t,e={}){if(this.config={...a,...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(){const[t,e,s]=await Promise.all([b(this.config.brushSrc),b(this.config.imageMaskSrc),b(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 t=P(i=>{this.updatePosition(i),this.scratch(),this.config.enabledPercentUpdate&&(this.percent=this.updatePercent(),this.config.onProgress?.(this.percent)),this.finish(i,t)}),e=i=>{i.preventDefault(),this.updatePosition(i),this.scratch(),this._canvas.setPointerCapture(i.pointerId),this._canvas.addEventListener("pointermove",t),this.config.enabledPercentUpdate&&(this.percent=this.updatePercent(),this.config.onProgress?.(this.percent)),this.finish(i,t)},s=i=>{this.finish(i,t)};this._canvas.addEventListener("pointerdown",e),this._canvas.addEventListener("pointerup",s),this._canvas.addEventListener("pointerleave",s),this.removeListeners=()=>{this._canvas.removeEventListener("pointerdown",e),this._canvas.removeEventListener("pointerup",s),this._canvas.removeEventListener("pointerleave",s),this._canvas.removeEventListener("pointermove",t)}}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(){this.brushImage&&(this.ctx.globalCompositeOperation="destination-out",this.ctx.save(),this.brush.brush(this.brushImage,this.brushSize),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.clear(),this._canvas.style.pointerEvents="none",this.config.onComplete?.(),t&&e)){try{this._canvas.releasePointerCapture(t.pointerId)}catch{}this._canvas.removeEventListener("pointermove",e)}}clear(){this.ctx.clearRect(0,0,this._canvas.width,this._canvas.height)}}function S(h="scratch-reveal"){if(typeof window>"u"||!("customElements"in window)||customElements.get(h))return;function t(i,n){if(i===null)return n;const r=i.trim().toLowerCase();return r===""?!0:r==="false"||r==="0"||r==="no"||r==="off"?!1:r==="true"||r==="1"||r==="yes"||r==="on"?!0:n}function e(i,n,r,c){if(!i)return c;const o=i.trim();if(!o)return c;if(o.endsWith("%")){const u=Number.parseFloat(o.slice(0,-1));if(!Number.isFinite(u))return c;const d=Math.min(n,r);return Math.max(0,d*u/100)}const l=o.endsWith("px")?Number.parseFloat(o.slice(0,-2)):Number.parseFloat(o);return Number.isFinite(l)?Math.max(0,l):c}class s extends HTMLElement{instance;container;styleEl=null;rebuildScheduled=!1;resizeObserver;static get observedAttributes(){return["width","height","percent-to-finish","brush-src","brush-size","mask-src","background-src","enabled-percent-update"]}constructor(){super();const n=this.attachShadow({mode:"open"});R(n)&&f?n.adoptedStyleSheets=[f]:(this.styleEl=document.createElement("style"),this.styleEl.textContent=m,n.append(this.styleEl)),this.container=document.createElement("div"),this.container.className="sr",n.append(this.container)}connectedCallback(){this.scheduleRebuild()}disconnectedCallback(){this.instance?.destroy(),this.instance=void 0,this.resizeObserver?.disconnect(),this.resizeObserver=void 0}attributeChangedCallback(n,r,c){r!==c&&this.scheduleRebuild()}scheduleRebuild(){this.rebuildScheduled||(this.rebuildScheduled=!0,queueMicrotask(()=>{this.rebuildScheduled=!1,this.isConnected&&this.rebuild()}))}rebuild(){this.container.replaceChildren(),this.instance?.destroy();const n=this.hasAttribute("width"),r=this.hasAttribute("height"),c=this.getBoundingClientRect(),o=Math.round(c.width),l=Math.round(c.height),u=n?Number(this.getAttribute("width")):o||a.width,d=r?Number(this.getAttribute("height")):l||a.height,w=Number(this.getAttribute("percent-to-finish")??a.percentToFinish),_=this.getAttribute("brush-src")??a.brushSrc,E=t(this.getAttribute("enabled-percent-update"),a.enabledPercentUpdate),y=e(this.getAttribute("brush-size"),u,d,a.brushSize),k=this.getAttribute("mask-src")??a.imageMaskSrc,x=this.getAttribute("background-src")??a.imageBackgroundSrc;n?this.container.style.width=`${u}px`:this.container.style.width="100%",r?this.container.style.height=`${d}px`:this.container.style.height="100%",this.instance=new M(this.container,{width:u,height:d,percentToFinish:w,brushSrc:_,brushSize:y,imageMaskSrc:k,imageBackgroundSrc:x,enabledPercentUpdate:E,onProgress:g=>{this.dispatchEvent(new CustomEvent("progress",{detail:{percent:g}}))},onComplete:()=>{this.dispatchEvent(new CustomEvent("complete",{detail:{percent:100}}))}}),this.instance.init(),(!n||!r)&&"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 g=this.getBoundingClientRect(),v=Math.round(g.width),p=Math.round(g.height);this.instance?.resize(v,p);const C=e(this.getAttribute("brush-size"),v,p,a.brushSize);this.instance?.setBrushSize(C)}),this.resizeObserver.observe(this)):(this.resizeObserver?.disconnect(),this.resizeObserver=void 0)}}customElements.define(h,s)}function L(h){S(),h.config.globalProperties.$scratchReveal=!0}exports.installScratchReveal=L;exports.registerScratchRevealElement=S;exports.scratchRevealCssText=A;
package/dist/index.es.js CHANGED
@@ -16,35 +16,28 @@ class C {
16
16
  }
17
17
  const s = Math.atan2(this.mouseY, this.mouseX);
18
18
  if (this.ctx.save(), this.ctx.translate(this.mouseX, this.mouseY), this.ctx.rotate(s), e > 0) {
19
- const i = e, r = e * (t.height / t.width);
20
- this.ctx.drawImage(t, -(i / 2), -(r / 2), i, r);
19
+ const i = e, n = e * (t.height / t.width);
20
+ this.ctx.drawImage(t, -(i / 2), -(n / 2), i, n);
21
21
  } else
22
22
  this.ctx.drawImage(t, -(t.width / 2), -(t.height / 2));
23
23
  this.ctx.restore();
24
24
  }
25
25
  }
26
- function b(n) {
26
+ function b(h) {
27
27
  return new Promise((t, e) => {
28
28
  const s = new Image();
29
- s.crossOrigin = "anonymous", s.onload = () => t(s), s.onerror = () => e(new Error(`Image ${n} failed to load`)), s.src = n;
29
+ s.crossOrigin = "anonymous", s.onload = () => t(s), s.onerror = () => e(new Error(`Image ${h} failed to load`)), s.src = h;
30
30
  });
31
31
  }
32
- function A(n) {
32
+ function z(h) {
33
33
  let t = 0;
34
34
  return ((...s) => {
35
35
  t || (t = requestAnimationFrame(() => {
36
- t = 0, n(...s);
36
+ t = 0, h(...s);
37
37
  }));
38
38
  });
39
39
  }
40
- function m(n) {
41
- const t = n.getBoundingClientRect();
42
- return {
43
- left: t.left + window.scrollX,
44
- top: t.top + window.scrollY
45
- };
46
- }
47
- const v = `.sr {
40
+ const m = `.sr {
48
41
  position: relative;
49
42
  overflow: hidden;
50
43
  width: 100%;
@@ -68,23 +61,23 @@ const v = `.sr {
68
61
 
69
62
  scratch-reveal {
70
63
  display: inline-block;
71
- width: 300px;
72
- height: 300px;
64
+ width: 100%;
65
+ height: 100%;
73
66
  }
74
- `, O = v;
75
- function P(n) {
76
- return "adoptedStyleSheets" in n;
67
+ `, L = m;
68
+ function A(h) {
69
+ return "adoptedStyleSheets" in h;
77
70
  }
78
- function I(n) {
71
+ function P(h) {
79
72
  if (typeof CSSStyleSheet > "u") return null;
80
73
  try {
81
74
  const t = new CSSStyleSheet();
82
- return t.replaceSync(n), t;
75
+ return t.replaceSync(h), t;
83
76
  } catch {
84
77
  return null;
85
78
  }
86
79
  }
87
- const w = I(v), a = {
80
+ const f = P(m), a = {
88
81
  width: 300,
89
82
  height: 300,
90
83
  brushSrc: "/demo/assets/brush.png",
@@ -94,7 +87,7 @@ const w = I(v), a = {
94
87
  percentToFinish: 60,
95
88
  enabledPercentUpdate: !0
96
89
  };
97
- class M {
90
+ class I {
98
91
  config;
99
92
  ctx;
100
93
  container;
@@ -108,7 +101,6 @@ class M {
108
101
  percent = 0;
109
102
  done = !1;
110
103
  destroyed = !1;
111
- zone = { top: 0, left: 0 };
112
104
  removeListeners;
113
105
  get canvas() {
114
106
  return this._canvas;
@@ -126,7 +118,7 @@ class M {
126
118
  b(this.config.imageMaskSrc),
127
119
  b(this.config.imageBackgroundSrc)
128
120
  ]);
129
- return this.destroyed ? this : (this.brushImage = t, this.maskImage = e, this.backgroundImage = s, this.drawMask(), this.setBackground(), this.zone = m(this._canvas), this.bindEvents(), this);
121
+ return this.destroyed ? this : (this.brushImage = t, this.maskImage = e, this.backgroundImage = s, this.drawMask(), this.setBackground(), this.bindEvents(), this);
130
122
  }
131
123
  destroy() {
132
124
  this.destroyed = !0, this.removeListeners?.();
@@ -139,16 +131,16 @@ class M {
139
131
  return s.className = "sr__canvas", s.width = t, s.height = e, s.style.width = "100%", s.style.height = "100%", s;
140
132
  }
141
133
  resize(t, e) {
142
- 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(), this.zone = m(this._canvas));
134
+ 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());
143
135
  }
144
136
  setBrushSize(t) {
145
137
  this.destroyed || !Number.isFinite(t) || t < 0 || (this.brushSize = t);
146
138
  }
147
139
  bindEvents() {
148
- const t = A((i) => {
140
+ const t = z((i) => {
149
141
  this.updatePosition(i), this.scratch(), this.config.enabledPercentUpdate && (this.percent = this.updatePercent(), this.config.onProgress?.(this.percent)), this.finish(i, t);
150
142
  }), e = (i) => {
151
- i.preventDefault(), this.zone = m(this._canvas), this.updatePosition(i), this.scratch(), this._canvas.setPointerCapture(i.pointerId), this._canvas.addEventListener("pointermove", t), this.config.enabledPercentUpdate && (this.percent = this.updatePercent(), this.config.onProgress?.(this.percent)), this.finish(i, t);
143
+ i.preventDefault(), this.updatePosition(i), this.scratch(), this._canvas.setPointerCapture(i.pointerId), this._canvas.addEventListener("pointermove", t), this.config.enabledPercentUpdate && (this.percent = this.updatePercent(), this.config.onProgress?.(this.percent)), this.finish(i, t);
152
144
  }, s = (i) => {
153
145
  this.finish(i, t);
154
146
  };
@@ -157,8 +149,8 @@ class M {
157
149
  };
158
150
  }
159
151
  updatePosition(t) {
160
- const e = t.clientX - this.zone.left, s = t.clientY - this.zone.top;
161
- this.brush.updateMousePosition(e, s);
152
+ 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;
153
+ this.brush.updateMousePosition(n, r);
162
154
  }
163
155
  drawMask() {
164
156
  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));
@@ -191,25 +183,25 @@ class M {
191
183
  this.ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
192
184
  }
193
185
  }
194
- function R(n = "scratch-reveal") {
195
- if (typeof window > "u" || !("customElements" in window) || customElements.get(n)) return;
196
- function t(i, r) {
197
- if (i === null) return r;
198
- const h = i.trim().toLowerCase();
199
- return h === "" ? !0 : h === "false" || h === "0" || h === "no" || h === "off" ? !1 : h === "true" || h === "1" || h === "yes" || h === "on" ? !0 : r;
186
+ function M(h = "scratch-reveal") {
187
+ if (typeof window > "u" || !("customElements" in window) || customElements.get(h)) return;
188
+ function t(i, n) {
189
+ if (i === null) return n;
190
+ const r = i.trim().toLowerCase();
191
+ return r === "" ? !0 : r === "false" || r === "0" || r === "no" || r === "off" ? !1 : r === "true" || r === "1" || r === "yes" || r === "on" ? !0 : n;
200
192
  }
201
- function e(i, r, h, o) {
202
- if (!i) return o;
203
- const c = i.trim();
204
- if (!c) return o;
205
- if (c.endsWith("%")) {
206
- const u = Number.parseFloat(c.slice(0, -1));
207
- if (!Number.isFinite(u)) return o;
208
- const d = Math.min(r, h);
193
+ function e(i, n, r, c) {
194
+ if (!i) return c;
195
+ const o = i.trim();
196
+ if (!o) return c;
197
+ if (o.endsWith("%")) {
198
+ const u = Number.parseFloat(o.slice(0, -1));
199
+ if (!Number.isFinite(u)) return c;
200
+ const d = Math.min(n, r);
209
201
  return Math.max(0, d * u / 100);
210
202
  }
211
- const l = c.endsWith("px") ? Number.parseFloat(c.slice(0, -2)) : Number.parseFloat(c);
212
- return Number.isFinite(l) ? Math.max(0, l) : o;
203
+ const l = o.endsWith("px") ? Number.parseFloat(o.slice(0, -2)) : Number.parseFloat(o);
204
+ return Number.isFinite(l) ? Math.max(0, l) : c;
213
205
  }
214
206
  class s extends HTMLElement {
215
207
  instance;
@@ -231,8 +223,8 @@ function R(n = "scratch-reveal") {
231
223
  }
232
224
  constructor() {
233
225
  super();
234
- const r = this.attachShadow({ mode: "open" });
235
- P(r) && w ? r.adoptedStyleSheets = [w] : (this.styleEl = document.createElement("style"), this.styleEl.textContent = v, r.append(this.styleEl)), this.container = document.createElement("div"), this.container.className = "sr", r.append(this.container);
226
+ const n = this.attachShadow({ mode: "open" });
227
+ A(n) && f ? n.adoptedStyleSheets = [f] : (this.styleEl = document.createElement("style"), this.styleEl.textContent = m, n.append(this.styleEl)), this.container = document.createElement("div"), this.container.className = "sr", n.append(this.container);
236
228
  }
237
229
  connectedCallback() {
238
230
  this.scheduleRebuild();
@@ -240,8 +232,8 @@ function R(n = "scratch-reveal") {
240
232
  disconnectedCallback() {
241
233
  this.instance?.destroy(), this.instance = void 0, this.resizeObserver?.disconnect(), this.resizeObserver = void 0;
242
234
  }
243
- attributeChangedCallback(r, h, o) {
244
- h !== o && this.scheduleRebuild();
235
+ attributeChangedCallback(n, r, c) {
236
+ r !== c && this.scheduleRebuild();
245
237
  }
246
238
  scheduleRebuild() {
247
239
  this.rebuildScheduled || (this.rebuildScheduled = !0, queueMicrotask(() => {
@@ -250,56 +242,56 @@ function R(n = "scratch-reveal") {
250
242
  }
251
243
  rebuild() {
252
244
  this.container.replaceChildren(), this.instance?.destroy();
253
- const r = this.hasAttribute("width"), h = this.hasAttribute("height"), o = this.getBoundingClientRect(), c = Math.round(o.width), l = Math.round(o.height), u = r ? Number(this.getAttribute("width")) : c || a.width, d = h ? Number(this.getAttribute("height")) : l || a.height, S = Number(
245
+ const n = this.hasAttribute("width"), r = this.hasAttribute("height"), c = this.getBoundingClientRect(), o = Math.round(c.width), l = Math.round(c.height), u = n ? Number(this.getAttribute("width")) : o || a.width, d = r ? Number(this.getAttribute("height")) : l || a.height, w = Number(
254
246
  this.getAttribute("percent-to-finish") ?? a.percentToFinish
255
- ), _ = this.getAttribute("brush-src") ?? a.brushSrc, E = t(
247
+ ), S = this.getAttribute("brush-src") ?? a.brushSrc, _ = t(
256
248
  this.getAttribute("enabled-percent-update"),
257
249
  a.enabledPercentUpdate
258
- ), y = e(
250
+ ), E = e(
259
251
  this.getAttribute("brush-size"),
260
252
  u,
261
253
  d,
262
254
  a.brushSize
263
- ), x = this.getAttribute("mask-src") ?? a.imageMaskSrc, k = this.getAttribute("background-src") ?? a.imageBackgroundSrc;
264
- r ? this.container.style.width = `${u}px` : this.container.style.width = "100%", h ? this.container.style.height = `${d}px` : this.container.style.height = "100%", this.instance = new M(this.container, {
255
+ ), y = this.getAttribute("mask-src") ?? a.imageMaskSrc, k = this.getAttribute("background-src") ?? a.imageBackgroundSrc;
256
+ n ? this.container.style.width = `${u}px` : this.container.style.width = "100%", r ? this.container.style.height = `${d}px` : this.container.style.height = "100%", this.instance = new I(this.container, {
265
257
  width: u,
266
258
  height: d,
267
- percentToFinish: S,
268
- brushSrc: _,
269
- brushSize: y,
270
- imageMaskSrc: x,
259
+ percentToFinish: w,
260
+ brushSrc: S,
261
+ brushSize: E,
262
+ imageMaskSrc: y,
271
263
  imageBackgroundSrc: k,
272
- enabledPercentUpdate: E,
264
+ enabledPercentUpdate: _,
273
265
  onProgress: (g) => {
274
266
  this.dispatchEvent(new CustomEvent("progress", { detail: { percent: g } }));
275
267
  },
276
268
  onComplete: () => {
277
269
  this.dispatchEvent(new CustomEvent("complete", { detail: { percent: 100 } }));
278
270
  }
279
- }), this.instance.init(), (!r || !h) && "ResizeObserver" in window ? (this.resizeObserver?.disconnect(), this.resizeObserver = new ResizeObserver(() => {
271
+ }), this.instance.init(), (!n || !r) && "ResizeObserver" in window ? (this.resizeObserver?.disconnect(), this.resizeObserver = new ResizeObserver(() => {
280
272
  if (this.hasAttribute("width") && this.hasAttribute("height")) {
281
273
  this.resizeObserver?.disconnect(), this.resizeObserver = void 0;
282
274
  return;
283
275
  }
284
- const g = this.getBoundingClientRect(), p = Math.round(g.width), f = Math.round(g.height);
285
- this.instance?.resize(p, f);
286
- const z = e(
276
+ const g = this.getBoundingClientRect(), v = Math.round(g.width), p = Math.round(g.height);
277
+ this.instance?.resize(v, p);
278
+ const x = e(
287
279
  this.getAttribute("brush-size"),
280
+ v,
288
281
  p,
289
- f,
290
282
  a.brushSize
291
283
  );
292
- this.instance?.setBrushSize(z);
284
+ this.instance?.setBrushSize(x);
293
285
  }), this.resizeObserver.observe(this)) : (this.resizeObserver?.disconnect(), this.resizeObserver = void 0);
294
286
  }
295
287
  }
296
- customElements.define(n, s);
288
+ customElements.define(h, s);
297
289
  }
298
- function B(n) {
299
- R(), n.config.globalProperties.$scratchReveal = !0;
290
+ function B(h) {
291
+ M(), h.config.globalProperties.$scratchReveal = !0;
300
292
  }
301
293
  export {
302
294
  B as installScratchReveal,
303
- R as registerScratchRevealElement,
304
- O as scratchRevealCssText
295
+ M as registerScratchRevealElement,
296
+ L as scratchRevealCssText
305
297
  };
package/dist/index.umd.js CHANGED
@@ -1,4 +1,4 @@
1
- (function(c,l){typeof exports=="object"&&typeof module<"u"?l(exports):typeof define=="function"&&define.amd?define(["exports"],l):(c=typeof globalThis<"u"?globalThis:c||self,l(c.ScratchReveal={}))})(this,(function(c){"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){const i=new Error("Brush.brush: img is required");console.log(i.message);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,r=e*(t.height/t.width);this.ctx.drawImage(t,-(i/2),-(r/2),i,r)}else this.ctx.drawImage(t,-(t.width/2),-(t.height/2));this.ctx.restore()}}function v(n){return new Promise((t,e)=>{const s=new Image;s.crossOrigin="anonymous",s.onload=()=>t(s),s.onerror=()=>e(new Error(`Image ${n} failed to load`)),s.src=n})}function y(n){let t=0;return((...s)=>{t||(t=requestAnimationFrame(()=>{t=0,n(...s)}))})}function f(n){const t=n.getBoundingClientRect();return{left:t.left+window.scrollX,top:t.top+window.scrollY}}const p=`.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){const i=new Error("Brush.brush: img is required");console.log(i.message);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 p=`.sr {
2
2
  position: relative;
3
3
  overflow: hidden;
4
4
  width: 100%;
@@ -22,7 +22,7 @@
22
22
 
23
23
  scratch-reveal {
24
24
  display: inline-block;
25
- width: 300px;
26
- height: 300px;
25
+ width: 100%;
26
+ height: 100%;
27
27
  }
28
- `,x=p;function k(n){return"adoptedStyleSheets"in n}function z(n){if(typeof CSSStyleSheet>"u")return null;try{const t=new CSSStyleSheet;return t.replaceSync(n),t}catch{return null}}const S=z(p),a={width:300,height:300,brushSrc:"/demo/assets/brush.png",imageMaskSrc:"/demo/assets/scratch-reveal.png",imageBackgroundSrc:"/demo/assets/scratch-reveal-background.svg",brushSize:0,percentToFinish:60,enabledPercentUpdate:!0};class C{config;ctx;container;_canvas;brush;maskImage;backgroundImage;brushImage;backgroundEl;brushSize=0;percent=0;done=!1;destroyed=!1;zone={top:0,left:0};removeListeners;get canvas(){return this._canvas}constructor(t,e={}){if(this.config={...a,...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(){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.zone=f(this._canvas),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(),this.zone=f(this._canvas))}setBrushSize(t){this.destroyed||!Number.isFinite(t)||t<0||(this.brushSize=t)}bindEvents(){const t=y(i=>{this.updatePosition(i),this.scratch(),this.config.enabledPercentUpdate&&(this.percent=this.updatePercent(),this.config.onProgress?.(this.percent)),this.finish(i,t)}),e=i=>{i.preventDefault(),this.zone=f(this._canvas),this.updatePosition(i),this.scratch(),this._canvas.setPointerCapture(i.pointerId),this._canvas.addEventListener("pointermove",t),this.config.enabledPercentUpdate&&(this.percent=this.updatePercent(),this.config.onProgress?.(this.percent)),this.finish(i,t)},s=i=>{this.finish(i,t)};this._canvas.addEventListener("pointerdown",e),this._canvas.addEventListener("pointerup",s),this._canvas.addEventListener("pointerleave",s),this.removeListeners=()=>{this._canvas.removeEventListener("pointerdown",e),this._canvas.removeEventListener("pointerup",s),this._canvas.removeEventListener("pointerleave",s),this._canvas.removeEventListener("pointermove",t)}}updatePosition(t){const e=t.clientX-this.zone.left,s=t.clientY-this.zone.top;this.brush.updateMousePosition(e,s)}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(){this.brushImage&&(this.ctx.globalCompositeOperation="destination-out",this.ctx.save(),this.brush.brush(this.brushImage,this.brushSize),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.clear(),this._canvas.style.pointerEvents="none",this.config.onComplete?.(),t&&e)){try{this._canvas.releasePointerCapture(t.pointerId)}catch{}this._canvas.removeEventListener("pointermove",e)}}clear(){this.ctx.clearRect(0,0,this._canvas.width,this._canvas.height)}}function w(n="scratch-reveal"){if(typeof window>"u"||!("customElements"in window)||customElements.get(n))return;function t(i,r){if(i===null)return r;const h=i.trim().toLowerCase();return h===""?!0:h==="false"||h==="0"||h==="no"||h==="off"?!1:h==="true"||h==="1"||h==="yes"||h==="on"?!0:r}function e(i,r,h,o){if(!i)return o;const u=i.trim();if(!u)return o;if(u.endsWith("%")){const d=Number.parseFloat(u.slice(0,-1));if(!Number.isFinite(d))return o;const g=Math.min(r,h);return Math.max(0,g*d/100)}const m=u.endsWith("px")?Number.parseFloat(u.slice(0,-2)):Number.parseFloat(u);return Number.isFinite(m)?Math.max(0,m):o}class s extends HTMLElement{instance;container;styleEl=null;rebuildScheduled=!1;resizeObserver;static get observedAttributes(){return["width","height","percent-to-finish","brush-src","brush-size","mask-src","background-src","enabled-percent-update"]}constructor(){super();const r=this.attachShadow({mode:"open"});k(r)&&S?r.adoptedStyleSheets=[S]:(this.styleEl=document.createElement("style"),this.styleEl.textContent=p,r.append(this.styleEl)),this.container=document.createElement("div"),this.container.className="sr",r.append(this.container)}connectedCallback(){this.scheduleRebuild()}disconnectedCallback(){this.instance?.destroy(),this.instance=void 0,this.resizeObserver?.disconnect(),this.resizeObserver=void 0}attributeChangedCallback(r,h,o){h!==o&&this.scheduleRebuild()}scheduleRebuild(){this.rebuildScheduled||(this.rebuildScheduled=!0,queueMicrotask(()=>{this.rebuildScheduled=!1,this.isConnected&&this.rebuild()}))}rebuild(){this.container.replaceChildren(),this.instance?.destroy();const r=this.hasAttribute("width"),h=this.hasAttribute("height"),o=this.getBoundingClientRect(),u=Math.round(o.width),m=Math.round(o.height),d=r?Number(this.getAttribute("width")):u||a.width,g=h?Number(this.getAttribute("height")):m||a.height,A=Number(this.getAttribute("percent-to-finish")??a.percentToFinish),R=this.getAttribute("brush-src")??a.brushSrc,I=t(this.getAttribute("enabled-percent-update"),a.enabledPercentUpdate),M=e(this.getAttribute("brush-size"),d,g,a.brushSize),T=this.getAttribute("mask-src")??a.imageMaskSrc,L=this.getAttribute("background-src")??a.imageBackgroundSrc;r?this.container.style.width=`${d}px`:this.container.style.width="100%",h?this.container.style.height=`${g}px`:this.container.style.height="100%",this.instance=new C(this.container,{width:d,height:g,percentToFinish:A,brushSrc:R,brushSize:M,imageMaskSrc:T,imageBackgroundSrc:L,enabledPercentUpdate:I,onProgress:b=>{this.dispatchEvent(new CustomEvent("progress",{detail:{percent:b}}))},onComplete:()=>{this.dispatchEvent(new CustomEvent("complete",{detail:{percent:100}}))}}),this.instance.init(),(!r||!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 b=this.getBoundingClientRect(),_=Math.round(b.width),E=Math.round(b.height);this.instance?.resize(_,E);const O=e(this.getAttribute("brush-size"),_,E,a.brushSize);this.instance?.setBrushSize(O)}),this.resizeObserver.observe(this)):(this.resizeObserver?.disconnect(),this.resizeObserver=void 0)}}customElements.define(n,s)}function P(n){w(),n.config.globalProperties.$scratchReveal=!0}c.installScratchReveal=P,c.registerScratchRevealElement=w,c.scratchRevealCssText=x,Object.defineProperty(c,Symbol.toStringTag,{value:"Module"})}));
28
+ `,y=p;function k(h){return"adoptedStyleSheets"in h}function x(h){if(typeof CSSStyleSheet>"u")return null;try{const t=new CSSStyleSheet;return t.replaceSync(h),t}catch{return null}}const f=x(p),a={width:300,height:300,brushSrc:"/demo/assets/brush.png",imageMaskSrc:"/demo/assets/scratch-reveal.png",imageBackgroundSrc:"/demo/assets/scratch-reveal-background.svg",brushSize:0,percentToFinish:60,enabledPercentUpdate:!0};class C{config;ctx;container;_canvas;brush;maskImage;backgroundImage;brushImage;backgroundEl;brushSize=0;percent=0;done=!1;destroyed=!1;removeListeners;get canvas(){return this._canvas}constructor(t,e={}){if(this.config={...a,...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(){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 t=E(i=>{this.updatePosition(i),this.scratch(),this.config.enabledPercentUpdate&&(this.percent=this.updatePercent(),this.config.onProgress?.(this.percent)),this.finish(i,t)}),e=i=>{i.preventDefault(),this.updatePosition(i),this.scratch(),this._canvas.setPointerCapture(i.pointerId),this._canvas.addEventListener("pointermove",t),this.config.enabledPercentUpdate&&(this.percent=this.updatePercent(),this.config.onProgress?.(this.percent)),this.finish(i,t)},s=i=>{this.finish(i,t)};this._canvas.addEventListener("pointerdown",e),this._canvas.addEventListener("pointerup",s),this._canvas.addEventListener("pointerleave",s),this.removeListeners=()=>{this._canvas.removeEventListener("pointerdown",e),this._canvas.removeEventListener("pointerup",s),this._canvas.removeEventListener("pointerleave",s),this._canvas.removeEventListener("pointermove",t)}}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(){this.brushImage&&(this.ctx.globalCompositeOperation="destination-out",this.ctx.save(),this.brush.brush(this.brushImage,this.brushSize),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.clear(),this._canvas.style.pointerEvents="none",this.config.onComplete?.(),t&&e)){try{this._canvas.releasePointerCapture(t.pointerId)}catch{}this._canvas.removeEventListener("pointermove",e)}}clear(){this.ctx.clearRect(0,0,this._canvas.width,this._canvas.height)}}function S(h="scratch-reveal"){if(typeof window>"u"||!("customElements"in window)||customElements.get(h))return;function t(i,n){if(i===null)return n;const r=i.trim().toLowerCase();return r===""?!0:r==="false"||r==="0"||r==="no"||r==="off"?!1:r==="true"||r==="1"||r==="yes"||r==="on"?!0:n}function e(i,n,r,c){if(!i)return c;const u=i.trim();if(!u)return c;if(u.endsWith("%")){const d=Number.parseFloat(u.slice(0,-1));if(!Number.isFinite(d))return c;const g=Math.min(n,r);return Math.max(0,g*d/100)}const m=u.endsWith("px")?Number.parseFloat(u.slice(0,-2)):Number.parseFloat(u);return Number.isFinite(m)?Math.max(0,m):c}class s extends HTMLElement{instance;container;styleEl=null;rebuildScheduled=!1;resizeObserver;static get observedAttributes(){return["width","height","percent-to-finish","brush-src","brush-size","mask-src","background-src","enabled-percent-update"]}constructor(){super();const n=this.attachShadow({mode:"open"});k(n)&&f?n.adoptedStyleSheets=[f]:(this.styleEl=document.createElement("style"),this.styleEl.textContent=p,n.append(this.styleEl)),this.container=document.createElement("div"),this.container.className="sr",n.append(this.container)}connectedCallback(){this.scheduleRebuild()}disconnectedCallback(){this.instance?.destroy(),this.instance=void 0,this.resizeObserver?.disconnect(),this.resizeObserver=void 0}attributeChangedCallback(n,r,c){r!==c&&this.scheduleRebuild()}scheduleRebuild(){this.rebuildScheduled||(this.rebuildScheduled=!0,queueMicrotask(()=>{this.rebuildScheduled=!1,this.isConnected&&this.rebuild()}))}rebuild(){this.container.replaceChildren(),this.instance?.destroy();const n=this.hasAttribute("width"),r=this.hasAttribute("height"),c=this.getBoundingClientRect(),u=Math.round(c.width),m=Math.round(c.height),d=n?Number(this.getAttribute("width")):u||a.width,g=r?Number(this.getAttribute("height")):m||a.height,P=Number(this.getAttribute("percent-to-finish")??a.percentToFinish),A=this.getAttribute("brush-src")??a.brushSrc,R=t(this.getAttribute("enabled-percent-update"),a.enabledPercentUpdate),I=e(this.getAttribute("brush-size"),d,g,a.brushSize),M=this.getAttribute("mask-src")??a.imageMaskSrc,T=this.getAttribute("background-src")??a.imageBackgroundSrc;n?this.container.style.width=`${d}px`:this.container.style.width="100%",r?this.container.style.height=`${g}px`:this.container.style.height="100%",this.instance=new C(this.container,{width:d,height:g,percentToFinish:P,brushSrc:A,brushSize:I,imageMaskSrc:M,imageBackgroundSrc:T,enabledPercentUpdate:R,onProgress:b=>{this.dispatchEvent(new CustomEvent("progress",{detail:{percent:b}}))},onComplete:()=>{this.dispatchEvent(new CustomEvent("complete",{detail:{percent:100}}))}}),this.instance.init(),(!n||!r)&&"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 b=this.getBoundingClientRect(),w=Math.round(b.width),_=Math.round(b.height);this.instance?.resize(w,_);const L=e(this.getAttribute("brush-size"),w,_,a.brushSize);this.instance?.setBrushSize(L)}),this.resizeObserver.observe(this)):(this.resizeObserver?.disconnect(),this.resizeObserver=void 0)}}customElements.define(h,s)}function z(h){S(),h.config.globalProperties.$scratchReveal=!0}o.installScratchReveal=z,o.registerScratchRevealElement=S,o.scratchRevealCssText=y,Object.defineProperty(o,Symbol.toStringTag,{value:"Module"})}));
package/dist/options.d.ts CHANGED
@@ -4,9 +4,6 @@ export interface ScratchRevealOptions {
4
4
  imageMaskSrc: string;
5
5
  imageBackgroundSrc: string;
6
6
  brushSrc: string;
7
- /**
8
- * Brush width in CSS pixels. Use `0` to keep the brush image natural size.
9
- */
10
7
  brushSize: number;
11
8
  percentToFinish: number;
12
9
  enabledPercentUpdate: boolean;
@@ -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:300px;height:300px;display:inline-block}
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}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scratch-reveal",
3
- "version": "1.0.0-dev.0",
3
+ "version": "1.0.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",