@craftily/image
Version:
A lightweight image filter utility for canvas using TypeScript.
104 lines (102 loc) • 7.8 kB
JavaScript
;var t=require("tslib"),e=require("lit"),a=require("lit/decorators.js");const o={brightness:100,contrast:100,saturate:100,grayscale:0,opacity:100,sepia:0,hueRotate:0,blur:0,color:void 0},l={brightness:{defaultValue:100,min:0,max:200},contrast:{defaultValue:100,min:0,max:200},saturate:{defaultValue:100,min:0,max:300},grayscale:{defaultValue:0,min:0,max:1},opacity:{defaultValue:100,min:0,max:100},sepia:{defaultValue:0,min:0,max:100},hueRotate:{defaultValue:0,min:0,max:360},blur:{defaultValue:0,min:0,max:10},color:{defaultValue:void 0}},i={png:"image/png",jpeg:"image/jpeg",jpg:"image/jpg",bmp:"image/bmp",webp:"image/webp"};const r=new class{constructor(){this.canvas=null}applyImageFilters(t,e,a,l=o){t&&e&&a&&(this.canvas=a,t.clearRect(0,0,a.width,a.height),t.filter=`\n brightness(${l.brightness}%) \n contrast(${l.contrast}%) \n grayscale(${l.grayscale}) \n saturate(${l.saturate}%) \n sepia(${l.sepia}) \n hue-rotate(${100*parseFloat(l.hueRotate)}deg)\n blur(${l.blur}px)\n opacity(${l.opacity}%) \n `.trim(),t.drawImage(e,0,0,a.width,a.height),l.color&&(t.globalCompositeOperation="source-atop",t.fillStyle=l.color,t.fillRect(0,0,a.width,a.height),t.globalCompositeOperation="source-over"))}toBlob(t="png",e=1){const a=i[t];if(!a)throw new Error(`Unsupported format: ${t}`);return new Promise(((t,o)=>{this.canvas.toBlob((e=>{e?t(e):o(new Error("Failed to create blob"))}),a,e)}))}toPng(t=1){return this.toBlob("png",t)}toJpeg(t=1){return this.toBlob("jpeg",t)}toJpg(t=1){return this.toBlob("jpg",t)}toBmp(t=1){return this.toBlob("bmp",t)}toWebp(t=1){return this.toBlob("webp",t)}toDataURL(t="png",e=1){const a=i[t];if(!a)throw new Error(`Unsupported format: ${t}`);return this.canvas.toDataURL(a,e)}downloadImage(t="png",e=1){const a=this.toDataURL(t,e),o=document.createElement("a");o.href=a,o.download="image",o.click()}download(t="png",e=1){this.downloadImage(t,e)}};var s=e.css`
:host {
display: block;
font-family: sans-serif;
max-width: 100%;
justify-self: center;
}
canvas {
max-width: 100%;
display: block;
margin-bottom: 1rem;
outline: 1px solid;
justify-self: center;
}
.toolbar {
display: inline-flex;
gap: 1rem;
padding: 1rem;
flex-direction: column;
width: 100%;
}
.control {
display: grid;
grid-template-columns: 110px 1fr 50px;
}
input[type='color'] {
margin-left: auto;
margin-right: auto;
width: 100%;
}
.preview {
margin-top: 1rem;
}
.preview img {
max-width: 100%;
display: block;
}
.upload {
margin-bottom: 1rem;
}
.actions {
display: grid;
grid-template-columns: 1fr 20px 50px;
gap: 1rem;
justify-content: end;
justify-items: start;
}
.actions.hide-download {
grid-template-columns: 1fr 50px;
}
.actions button {
background: none;
border: none;
cursor: pointer;
font-size: 1.5rem;
display: inline-block;
line-height: 1;
}
.actions button:hover {
opacity: 0.8;
}
.actions .icon {
display: inline-block;
line-height: 1;
}
.actions .divider {
background: #dddddd;
width: 100%;
}
`;class n extends e.LitElement{constructor(){super(...arguments),this.src="",this.controls=void 0,this.defaultState=l,this.format="png",this.mimeTypes=i}connectedCallback(){super.connectedCallback(),this.defaultState=Object.entries(l).reduce(((t,[e,a])=>(t[e]={defaultValue:this.controls?.[e]?.value||a.defaultValue,min:a?.min,max:a?.max},t)),{})}render(){return e.html`
${this.showUpload?e.html`<div class="upload" part="upload">
<label>Select Image:</label>
<input type="file" part="file-input" accept="image/*" @change=${this.onFileChange} />
</div>`:""}
<canvas part="canvas"></canvas>
<img src=${this.src} crossorigin="anonymous" @load=${this.draw} style="display:none;" />
<div class="toolbar" part="toolbar">
${Object.entries(this.controls||{}).map((([t,e])=>this.inputControl(t,e)))}
</div>
`}inputControl(t,a){if(this.src)return"color"===t?e.html`<div class="control" part="control">
<label>${a.label}</label>
<input
name=${t}
type="color"
part="color-picker"
value="${a.value||this.defaultState[t].defaultValue}"
@input=${e=>this.updateValue(t,e.target.value)}
/>
</div>`:e.html`<div class="control" part="control">
<label>${a.label}</label>
<input
name=${t}
type="range"
part="input-range"
min="${this.defaultState[t].min}"
max="${this.defaultState[t].max}"
step="0.1"
value="${a.value||this.defaultState[t].defaultValue}"
@input=${e=>this.updateValue(t,e.target.value)}
/>
</div>`}updateValue(t,e){this.controls[t].value=e,this.draw(),this.onControlChange(`${t}Changed`)}draw(){const t=this.canvas.getContext("2d"),e=this.imgEl;if(!t||!e)return;const a=this.width||e.naturalWidth,o=this.height||e.naturalHeight;this.canvas.width=a,this.canvas.height=o,r.applyImageFilters(t,e,this.canvas,{brightness:this.controls?.brightness?.value||this.defaultState.brightness.defaultValue,contrast:this.controls?.contrast?.value||this.defaultState.contrast.defaultValue,saturate:this.controls?.saturate?.value||this.defaultState.saturate.defaultValue,grayscale:this.controls?.grayscale?.value||this.defaultState.grayscale.defaultValue,opacity:this.controls?.opacity?.value||this.defaultState.opacity.defaultValue,sepia:this.controls?.sepia?.value||this.defaultState.sepia.defaultValue,hueRotate:this.controls?.hueRotate?.value||this.defaultState.hueRotate.defaultValue,blur:this.controls?.blur?.value||this.defaultState.blur.defaultValue,color:this.controls?.color?.value||this.defaultState.color.defaultValue})}download(t="png",e=1){r.download(t,e)}onFileChange(t){const e=t.target,a=e.files?.[0];a&&(this.src=URL.createObjectURL(a))}reset(){this.controls=Object.entries(this.controls||{}).reduce(((t,[e,a])=>{const o=this.renderRoot.querySelector(`input[name="${e}"]`);return o&&a.value!==this.defaultState[e].defaultValue&&(o.value=this.defaultState[e]?.defaultValue),t[e]={...a,value:this.defaultState[e].defaultValue},t}),{}),this.draw(),this.onControlChange("image-reset")}onControlChange(t){const e=new CustomEvent("onControlChange",{detail:{toDataURL:(t="png",e=1)=>r.toDataURL(t,e),toBlob:(t="png",e=1)=>r.toBlob(t,e),download:(t="png",e=1)=>r.download(t,e),metadata:{canvas:this.canvas,controls:this.controls,eventType:t}},bubbles:!0,composed:!0});this.dispatchEvent(e)}}n.styles=s,t.__decorate([a.property({type:String}),t.__metadata("design:type",Object)],n.prototype,"src",void 0),t.__decorate([a.property({type:Object}),t.__metadata("design:type",Object)],n.prototype,"controls",void 0),t.__decorate([a.property({type:Number}),t.__metadata("design:type",Number)],n.prototype,"width",void 0),t.__decorate([a.property({type:Number}),t.__metadata("design:type",Number)],n.prototype,"height",void 0),t.__decorate([a.property({type:Boolean}),t.__metadata("design:type",Boolean)],n.prototype,"showUpload",void 0),t.__decorate([a.state(),t.__metadata("design:type",Object)],n.prototype,"defaultState",void 0),t.__decorate([a.state(),t.__metadata("design:type",String)],n.prototype,"format",void 0),t.__decorate([a.query("canvas"),t.__metadata("design:type",HTMLCanvasElement)],n.prototype,"canvas",void 0),t.__decorate([a.query("img"),t.__metadata("design:type",HTMLImageElement)],n.prototype,"imgEl",void 0),t.__decorate([a.state(),t.__metadata("design:type",Object)],n.prototype,"mimeTypes",void 0),customElements.get("craftily-image-editor")||customElements.define("craftily-image-editor",n);
//# sourceMappingURL=ImageEditor.cjs.map