scratch-reveal 1.0.0-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/LICENSE +21 -0
- package/README.md +109 -0
- package/dist/index.cjs.js +28 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.es.js +305 -0
- package/dist/index.umd.js +28 -0
- package/dist/internal/Brush.d.ts +8 -0
- package/dist/internal/utils.d.ts +6 -0
- package/dist/options.d.ts +15 -0
- package/dist/scratch-reveal.css +1 -0
- package/dist/scratch-reveal.d.ts +5 -0
- package/package.json +65 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 ux-ui.pro
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
<p align="center"><strong>scratch-reveal</strong></p>
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
<p align="center">Web Component for scratch-to-reveal: canvas mask, background under it, custom brush. Ready for plain browser usage and Vue 3.</p>
|
|
5
|
+
|
|
6
|
+
[](https://www.npmjs.com/package/scratch-reveal)
|
|
7
|
+
[](https://github.com/ux-ui-pro/scratch-reveal)
|
|
8
|
+
[](https://www.npmjs.org/package/scratch-reveal)
|
|
9
|
+
|
|
10
|
+
<a href="https://codepen.io/ux-ui/pen/WbxvZqY">Demo</a>
|
|
11
|
+
</div>
|
|
12
|
+
<br>
|
|
13
|
+
|
|
14
|
+
➠ **Install**
|
|
15
|
+
```console
|
|
16
|
+
yarn add scratch-reveal
|
|
17
|
+
```
|
|
18
|
+
<br>
|
|
19
|
+
|
|
20
|
+
➠ **Register**
|
|
21
|
+
```ts
|
|
22
|
+
// registers <scratch-reveal>
|
|
23
|
+
import { registerScratchRevealElement } from 'scratch-reveal';
|
|
24
|
+
registerScratchRevealElement();
|
|
25
|
+
|
|
26
|
+
// Vue 3 plugin
|
|
27
|
+
import { createApp } from 'vue';
|
|
28
|
+
import { installScratchReveal } from 'scratch-reveal';
|
|
29
|
+
const app = createApp(App);
|
|
30
|
+
installScratchReveal(app);
|
|
31
|
+
app.mount('#app');
|
|
32
|
+
```
|
|
33
|
+
<br>
|
|
34
|
+
|
|
35
|
+
➠ **Usage (HTML)**
|
|
36
|
+
```html
|
|
37
|
+
<scratch-reveal
|
|
38
|
+
width="300"
|
|
39
|
+
height="300"
|
|
40
|
+
percent-to-finish="60"
|
|
41
|
+
brush-src="/demo/assets/brush.png"
|
|
42
|
+
brush-size="15%"
|
|
43
|
+
mask-src="/demo/assets/scratch-reveal.png"
|
|
44
|
+
background-src="/demo/assets/scratch-reveal-background.svg"
|
|
45
|
+
></scratch-reveal>
|
|
46
|
+
```
|
|
47
|
+
<br>
|
|
48
|
+
|
|
49
|
+
➠ **Auto-size (follow parent/container size)**
|
|
50
|
+
```html
|
|
51
|
+
<div style="width: 420px; height: 240px;">
|
|
52
|
+
<scratch-reveal
|
|
53
|
+
style="width: 100%; height: 100%;"
|
|
54
|
+
percent-to-finish="60"
|
|
55
|
+
brush-src="/demo/assets/brush.png"
|
|
56
|
+
brush-size="12%"
|
|
57
|
+
mask-src="/demo/assets/scratch-reveal.png"
|
|
58
|
+
background-src="/demo/assets/scratch-reveal-background.svg"
|
|
59
|
+
></scratch-reveal>
|
|
60
|
+
</div>
|
|
61
|
+
```
|
|
62
|
+
— If `width`/`height` attributes are omitted, the component will observe its own size and resize the canvas accordingly.
|
|
63
|
+
<br>
|
|
64
|
+
|
|
65
|
+
➠ **Events**
|
|
66
|
+
```js
|
|
67
|
+
const el = document.querySelector('scratch-reveal');
|
|
68
|
+
el.addEventListener('progress', (event) => {
|
|
69
|
+
console.log(event.detail.percent);
|
|
70
|
+
});
|
|
71
|
+
el.addEventListener('complete', () => {
|
|
72
|
+
console.log('done!');
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
— `progress` (detail: `{ percent: number }`)
|
|
76
|
+
— `complete` (detail: `{ percent: 100 }`)
|
|
77
|
+
<br>
|
|
78
|
+
|
|
79
|
+
➠ **Attributes**
|
|
80
|
+
|
|
81
|
+
| Attribute | Type | Default | Description |
|
|
82
|
+
|:------------------------:|:------------------:|:--------------------------------------------:|:---------------------------------------------------------------------------------------------------------|
|
|
83
|
+
| `width` / `height` | `number` | `300` | Container/mask size in px. If omitted, size follows layout (auto-size). |
|
|
84
|
+
| `percent-to-finish` | `number` | `60` | Percent cleared to consider done. |
|
|
85
|
+
| `brush-src` | `string` | `/demo/assets/brush.png` | Brush image. |
|
|
86
|
+
| `brush-size` | `string \| number` | `0` | Brush width: `80`/`80px` (px) or `12%` (percent of min(canvas width, height)). `0` = natural image size. |
|
|
87
|
+
| `mask-src` | `string` | `/demo/assets/scratch-reveal.png` | Top mask (scratched away). |
|
|
88
|
+
| `background-src` | `string` | `/demo/assets/scratch-reveal-background.svg` | Background beneath the mask. |
|
|
89
|
+
| `enabled-percent-update` | `boolean` | `true` | Compute cleared percent (used for `progress` and threshold checks). |
|
|
90
|
+
<br>
|
|
91
|
+
|
|
92
|
+
➠ **Styles**
|
|
93
|
+
- Shadow styles via constructable stylesheet with `<style>` fallback for older browsers.
|
|
94
|
+
- Reuse the shipped CSS text:
|
|
95
|
+
```ts
|
|
96
|
+
import { scratchRevealCssText } from 'scratch-reveal';
|
|
97
|
+
// apply wherever you need
|
|
98
|
+
```
|
|
99
|
+
<br>
|
|
100
|
+
|
|
101
|
+
➠ **Vue 3.5+**
|
|
102
|
+
- Register the custom element (see above).
|
|
103
|
+
- Optionally set `isCustomElement` in `vite.config.ts` for Volar/templates, or rely on the shipped `src/vue.d.ts` (global component `scratch-reveal`).
|
|
104
|
+
|
|
105
|
+
<br>
|
|
106
|
+
|
|
107
|
+
➠ **License**
|
|
108
|
+
|
|
109
|
+
MIT
|
|
@@ -0,0 +1,28 @@
|
|
|
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 {
|
|
2
|
+
position: relative;
|
|
3
|
+
overflow: hidden;
|
|
4
|
+
width: 100%;
|
|
5
|
+
height: 100%;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.sr__bg {
|
|
9
|
+
position: relative;
|
|
10
|
+
display: block;
|
|
11
|
+
width: 100%;
|
|
12
|
+
height: 100%;
|
|
13
|
+
object-fit: cover;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.sr__canvas {
|
|
17
|
+
position: absolute;
|
|
18
|
+
inset: 0;
|
|
19
|
+
width: 100%;
|
|
20
|
+
height: 100%;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
scratch-reveal {
|
|
24
|
+
display: inline-block;
|
|
25
|
+
width: 300px;
|
|
26
|
+
height: 300px;
|
|
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;
|
package/dist/index.d.ts
ADDED
package/dist/index.es.js
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
class C {
|
|
2
|
+
ctx;
|
|
3
|
+
mouseX;
|
|
4
|
+
mouseY;
|
|
5
|
+
constructor(t, e, s) {
|
|
6
|
+
this.ctx = t, this.mouseX = e, this.mouseY = s;
|
|
7
|
+
}
|
|
8
|
+
updateMousePosition(t, e) {
|
|
9
|
+
this.mouseX = t, this.mouseY = e;
|
|
10
|
+
}
|
|
11
|
+
brush(t, e = 0) {
|
|
12
|
+
if (!t) {
|
|
13
|
+
const i = new Error("Brush.brush: img is required");
|
|
14
|
+
console.log(i.message);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const s = Math.atan2(this.mouseY, this.mouseX);
|
|
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);
|
|
21
|
+
} else
|
|
22
|
+
this.ctx.drawImage(t, -(t.width / 2), -(t.height / 2));
|
|
23
|
+
this.ctx.restore();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function b(n) {
|
|
27
|
+
return new Promise((t, e) => {
|
|
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;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function A(n) {
|
|
33
|
+
let t = 0;
|
|
34
|
+
return ((...s) => {
|
|
35
|
+
t || (t = requestAnimationFrame(() => {
|
|
36
|
+
t = 0, n(...s);
|
|
37
|
+
}));
|
|
38
|
+
});
|
|
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 {
|
|
48
|
+
position: relative;
|
|
49
|
+
overflow: hidden;
|
|
50
|
+
width: 100%;
|
|
51
|
+
height: 100%;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.sr__bg {
|
|
55
|
+
position: relative;
|
|
56
|
+
display: block;
|
|
57
|
+
width: 100%;
|
|
58
|
+
height: 100%;
|
|
59
|
+
object-fit: cover;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.sr__canvas {
|
|
63
|
+
position: absolute;
|
|
64
|
+
inset: 0;
|
|
65
|
+
width: 100%;
|
|
66
|
+
height: 100%;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
scratch-reveal {
|
|
70
|
+
display: inline-block;
|
|
71
|
+
width: 300px;
|
|
72
|
+
height: 300px;
|
|
73
|
+
}
|
|
74
|
+
`, O = v;
|
|
75
|
+
function P(n) {
|
|
76
|
+
return "adoptedStyleSheets" in n;
|
|
77
|
+
}
|
|
78
|
+
function I(n) {
|
|
79
|
+
if (typeof CSSStyleSheet > "u") return null;
|
|
80
|
+
try {
|
|
81
|
+
const t = new CSSStyleSheet();
|
|
82
|
+
return t.replaceSync(n), t;
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const w = I(v), a = {
|
|
88
|
+
width: 300,
|
|
89
|
+
height: 300,
|
|
90
|
+
brushSrc: "/demo/assets/brush.png",
|
|
91
|
+
imageMaskSrc: "/demo/assets/scratch-reveal.png",
|
|
92
|
+
imageBackgroundSrc: "/demo/assets/scratch-reveal-background.svg",
|
|
93
|
+
brushSize: 0,
|
|
94
|
+
percentToFinish: 60,
|
|
95
|
+
enabledPercentUpdate: !0
|
|
96
|
+
};
|
|
97
|
+
class M {
|
|
98
|
+
config;
|
|
99
|
+
ctx;
|
|
100
|
+
container;
|
|
101
|
+
_canvas;
|
|
102
|
+
brush;
|
|
103
|
+
maskImage;
|
|
104
|
+
backgroundImage;
|
|
105
|
+
brushImage;
|
|
106
|
+
backgroundEl;
|
|
107
|
+
brushSize = 0;
|
|
108
|
+
percent = 0;
|
|
109
|
+
done = !1;
|
|
110
|
+
destroyed = !1;
|
|
111
|
+
zone = { top: 0, left: 0 };
|
|
112
|
+
removeListeners;
|
|
113
|
+
get canvas() {
|
|
114
|
+
return this._canvas;
|
|
115
|
+
}
|
|
116
|
+
constructor(t, e = {}) {
|
|
117
|
+
if (this.config = { ...a, ...e }, this.container = typeof t == "string" ? document.querySelector(t) : t, !this.container)
|
|
118
|
+
throw new Error("ScratchReveal: container not found");
|
|
119
|
+
this._canvas = this.createCanvas(this.config.width, this.config.height), this.ctx = this._canvas.getContext("2d", {
|
|
120
|
+
willReadFrequently: !0
|
|
121
|
+
}), this.brush = new C(this.ctx, 0, 0), this.brushSize = this.config.brushSize, this.container.appendChild(this._canvas);
|
|
122
|
+
}
|
|
123
|
+
async init() {
|
|
124
|
+
const [t, e, s] = await Promise.all([
|
|
125
|
+
b(this.config.brushSrc),
|
|
126
|
+
b(this.config.imageMaskSrc),
|
|
127
|
+
b(this.config.imageBackgroundSrc)
|
|
128
|
+
]);
|
|
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);
|
|
130
|
+
}
|
|
131
|
+
destroy() {
|
|
132
|
+
this.destroyed = !0, this.removeListeners?.();
|
|
133
|
+
}
|
|
134
|
+
getPercent() {
|
|
135
|
+
return this.percent;
|
|
136
|
+
}
|
|
137
|
+
createCanvas(t, e) {
|
|
138
|
+
const s = document.createElement("canvas");
|
|
139
|
+
return s.className = "sr__canvas", s.width = t, s.height = e, s.style.width = "100%", s.style.height = "100%", s;
|
|
140
|
+
}
|
|
141
|
+
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));
|
|
143
|
+
}
|
|
144
|
+
setBrushSize(t) {
|
|
145
|
+
this.destroyed || !Number.isFinite(t) || t < 0 || (this.brushSize = t);
|
|
146
|
+
}
|
|
147
|
+
bindEvents() {
|
|
148
|
+
const t = A((i) => {
|
|
149
|
+
this.updatePosition(i), this.scratch(), this.config.enabledPercentUpdate && (this.percent = this.updatePercent(), this.config.onProgress?.(this.percent)), this.finish(i, t);
|
|
150
|
+
}), 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);
|
|
152
|
+
}, s = (i) => {
|
|
153
|
+
this.finish(i, t);
|
|
154
|
+
};
|
|
155
|
+
this._canvas.addEventListener("pointerdown", e), this._canvas.addEventListener("pointerup", s), this._canvas.addEventListener("pointerleave", s), this.removeListeners = () => {
|
|
156
|
+
this._canvas.removeEventListener("pointerdown", e), this._canvas.removeEventListener("pointerup", s), this._canvas.removeEventListener("pointerleave", s), this._canvas.removeEventListener("pointermove", t);
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
updatePosition(t) {
|
|
160
|
+
const e = t.clientX - this.zone.left, s = t.clientY - this.zone.top;
|
|
161
|
+
this.brush.updateMousePosition(e, s);
|
|
162
|
+
}
|
|
163
|
+
drawMask() {
|
|
164
|
+
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));
|
|
165
|
+
}
|
|
166
|
+
setBackground() {
|
|
167
|
+
if (this.destroyed || !this.backgroundImage || !this.container.contains(this._canvas)) return;
|
|
168
|
+
const t = this.backgroundEl ?? document.createElement("img");
|
|
169
|
+
t.src = this.backgroundImage.src, t.className = "sr__bg", t.isConnected || this.container.insertBefore(t, this._canvas), this.backgroundEl = t;
|
|
170
|
+
}
|
|
171
|
+
scratch() {
|
|
172
|
+
this.brushImage && (this.ctx.globalCompositeOperation = "destination-out", this.ctx.save(), this.brush.brush(this.brushImage, this.brushSize), this.ctx.restore());
|
|
173
|
+
}
|
|
174
|
+
updatePercent() {
|
|
175
|
+
const e = this.ctx.getImageData(0, 0, this._canvas.width, this._canvas.height).data;
|
|
176
|
+
let s = 0;
|
|
177
|
+
for (let i = 3; i < e.length; i += 4)
|
|
178
|
+
e[i] === 0 && s++;
|
|
179
|
+
return s / (this._canvas.width * this._canvas.height) * 100;
|
|
180
|
+
}
|
|
181
|
+
finish(t, e) {
|
|
182
|
+
if (!this.done && this.percent > this.config.percentToFinish && (this.done = !0, this.clear(), this._canvas.style.pointerEvents = "none", this.config.onComplete?.(), t && e)) {
|
|
183
|
+
try {
|
|
184
|
+
this._canvas.releasePointerCapture(t.pointerId);
|
|
185
|
+
} catch {
|
|
186
|
+
}
|
|
187
|
+
this._canvas.removeEventListener("pointermove", e);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
clear() {
|
|
191
|
+
this.ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
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;
|
|
200
|
+
}
|
|
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);
|
|
209
|
+
return Math.max(0, d * u / 100);
|
|
210
|
+
}
|
|
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;
|
|
213
|
+
}
|
|
214
|
+
class s extends HTMLElement {
|
|
215
|
+
instance;
|
|
216
|
+
container;
|
|
217
|
+
styleEl = null;
|
|
218
|
+
rebuildScheduled = !1;
|
|
219
|
+
resizeObserver;
|
|
220
|
+
static get observedAttributes() {
|
|
221
|
+
return [
|
|
222
|
+
"width",
|
|
223
|
+
"height",
|
|
224
|
+
"percent-to-finish",
|
|
225
|
+
"brush-src",
|
|
226
|
+
"brush-size",
|
|
227
|
+
"mask-src",
|
|
228
|
+
"background-src",
|
|
229
|
+
"enabled-percent-update"
|
|
230
|
+
];
|
|
231
|
+
}
|
|
232
|
+
constructor() {
|
|
233
|
+
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);
|
|
236
|
+
}
|
|
237
|
+
connectedCallback() {
|
|
238
|
+
this.scheduleRebuild();
|
|
239
|
+
}
|
|
240
|
+
disconnectedCallback() {
|
|
241
|
+
this.instance?.destroy(), this.instance = void 0, this.resizeObserver?.disconnect(), this.resizeObserver = void 0;
|
|
242
|
+
}
|
|
243
|
+
attributeChangedCallback(r, h, o) {
|
|
244
|
+
h !== o && this.scheduleRebuild();
|
|
245
|
+
}
|
|
246
|
+
scheduleRebuild() {
|
|
247
|
+
this.rebuildScheduled || (this.rebuildScheduled = !0, queueMicrotask(() => {
|
|
248
|
+
this.rebuildScheduled = !1, this.isConnected && this.rebuild();
|
|
249
|
+
}));
|
|
250
|
+
}
|
|
251
|
+
rebuild() {
|
|
252
|
+
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(
|
|
254
|
+
this.getAttribute("percent-to-finish") ?? a.percentToFinish
|
|
255
|
+
), _ = this.getAttribute("brush-src") ?? a.brushSrc, E = t(
|
|
256
|
+
this.getAttribute("enabled-percent-update"),
|
|
257
|
+
a.enabledPercentUpdate
|
|
258
|
+
), y = e(
|
|
259
|
+
this.getAttribute("brush-size"),
|
|
260
|
+
u,
|
|
261
|
+
d,
|
|
262
|
+
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, {
|
|
265
|
+
width: u,
|
|
266
|
+
height: d,
|
|
267
|
+
percentToFinish: S,
|
|
268
|
+
brushSrc: _,
|
|
269
|
+
brushSize: y,
|
|
270
|
+
imageMaskSrc: x,
|
|
271
|
+
imageBackgroundSrc: k,
|
|
272
|
+
enabledPercentUpdate: E,
|
|
273
|
+
onProgress: (g) => {
|
|
274
|
+
this.dispatchEvent(new CustomEvent("progress", { detail: { percent: g } }));
|
|
275
|
+
},
|
|
276
|
+
onComplete: () => {
|
|
277
|
+
this.dispatchEvent(new CustomEvent("complete", { detail: { percent: 100 } }));
|
|
278
|
+
}
|
|
279
|
+
}), this.instance.init(), (!r || !h) && "ResizeObserver" in window ? (this.resizeObserver?.disconnect(), this.resizeObserver = new ResizeObserver(() => {
|
|
280
|
+
if (this.hasAttribute("width") && this.hasAttribute("height")) {
|
|
281
|
+
this.resizeObserver?.disconnect(), this.resizeObserver = void 0;
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
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(
|
|
287
|
+
this.getAttribute("brush-size"),
|
|
288
|
+
p,
|
|
289
|
+
f,
|
|
290
|
+
a.brushSize
|
|
291
|
+
);
|
|
292
|
+
this.instance?.setBrushSize(z);
|
|
293
|
+
}), this.resizeObserver.observe(this)) : (this.resizeObserver?.disconnect(), this.resizeObserver = void 0);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
customElements.define(n, s);
|
|
297
|
+
}
|
|
298
|
+
function B(n) {
|
|
299
|
+
R(), n.config.globalProperties.$scratchReveal = !0;
|
|
300
|
+
}
|
|
301
|
+
export {
|
|
302
|
+
B as installScratchReveal,
|
|
303
|
+
R as registerScratchRevealElement,
|
|
304
|
+
O as scratchRevealCssText
|
|
305
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
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 {
|
|
2
|
+
position: relative;
|
|
3
|
+
overflow: hidden;
|
|
4
|
+
width: 100%;
|
|
5
|
+
height: 100%;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.sr__bg {
|
|
9
|
+
position: relative;
|
|
10
|
+
display: block;
|
|
11
|
+
width: 100%;
|
|
12
|
+
height: 100%;
|
|
13
|
+
object-fit: cover;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.sr__canvas {
|
|
17
|
+
position: absolute;
|
|
18
|
+
inset: 0;
|
|
19
|
+
width: 100%;
|
|
20
|
+
height: 100%;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
scratch-reveal {
|
|
24
|
+
display: inline-block;
|
|
25
|
+
width: 300px;
|
|
26
|
+
height: 300px;
|
|
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"})}));
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export default class Brush {
|
|
2
|
+
readonly ctx: CanvasRenderingContext2D;
|
|
3
|
+
mouseX: number;
|
|
4
|
+
mouseY: number;
|
|
5
|
+
constructor(ctx: CanvasRenderingContext2D, mouseX: number, mouseY: number);
|
|
6
|
+
updateMousePosition(x: number, y: number): void;
|
|
7
|
+
brush(img: HTMLImageElement, size?: number): void;
|
|
8
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface ScratchRevealOptions {
|
|
2
|
+
width: number;
|
|
3
|
+
height: number;
|
|
4
|
+
imageMaskSrc: string;
|
|
5
|
+
imageBackgroundSrc: string;
|
|
6
|
+
brushSrc: string;
|
|
7
|
+
/**
|
|
8
|
+
* Brush width in CSS pixels. Use `0` to keep the brush image natural size.
|
|
9
|
+
*/
|
|
10
|
+
brushSize: number;
|
|
11
|
+
percentToFinish: number;
|
|
12
|
+
enabledPercentUpdate: boolean;
|
|
13
|
+
onProgress?: (percent: number) => void;
|
|
14
|
+
onComplete?: () => void;
|
|
15
|
+
}
|
|
@@ -0,0 +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}
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "scratch-reveal",
|
|
3
|
+
"version": "1.0.0-dev.0",
|
|
4
|
+
"description": "Scratch & reveal Web Component: mask + background with brush-only reveal, Vue-friendly, shadow-styled.",
|
|
5
|
+
"author": "ux-ui.pro",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/ux-ui-pro/scratch-reveal.git"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/ux-ui-pro/scratch-reveal/issues"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/ux-ui-pro/scratch-reveal",
|
|
15
|
+
"sideEffects": false,
|
|
16
|
+
"scripts": {
|
|
17
|
+
"clean": "rimraf dist",
|
|
18
|
+
"build": "vite build",
|
|
19
|
+
"dev": "vite",
|
|
20
|
+
"gen:brush:spray": "node scripts/gen-spray-brush.mjs",
|
|
21
|
+
"verify": "yarn lint && yarn typecheck",
|
|
22
|
+
"lint": "biome check src",
|
|
23
|
+
"lint:fix": "biome check --write src",
|
|
24
|
+
"format": "biome format --write src",
|
|
25
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
26
|
+
},
|
|
27
|
+
"source": "src/scratch-reveal.ts",
|
|
28
|
+
"main": "dist/index.cjs.js",
|
|
29
|
+
"module": "dist/index.es.js",
|
|
30
|
+
"types": "dist/index.d.ts",
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"types": "./dist/index.d.ts",
|
|
34
|
+
"import": "./dist/index.es.js",
|
|
35
|
+
"require": "./dist/index.cjs.js"
|
|
36
|
+
},
|
|
37
|
+
"./scratch-reveal.css": "./dist/scratch-reveal.css",
|
|
38
|
+
"./dist/*": "./dist/*"
|
|
39
|
+
},
|
|
40
|
+
"files": [
|
|
41
|
+
"dist/"
|
|
42
|
+
],
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@biomejs/biome": "2.3.10",
|
|
45
|
+
"@types/node": "25.0.3",
|
|
46
|
+
"lightningcss": "1.30.2",
|
|
47
|
+
"rimraf": "6.1.2",
|
|
48
|
+
"typescript": "5.9.3",
|
|
49
|
+
"vite": "7.3.0",
|
|
50
|
+
"vite-plugin-dts": "4.5.4"
|
|
51
|
+
},
|
|
52
|
+
"keywords": [
|
|
53
|
+
"typescript",
|
|
54
|
+
"frontend",
|
|
55
|
+
"library",
|
|
56
|
+
"scratch",
|
|
57
|
+
"scratch card",
|
|
58
|
+
"scratch reveal",
|
|
59
|
+
"canvas",
|
|
60
|
+
"game",
|
|
61
|
+
"ui",
|
|
62
|
+
"component",
|
|
63
|
+
"interactive"
|
|
64
|
+
]
|
|
65
|
+
}
|