diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2d02da7..cd1f809 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -24,3 +24,4 @@
- add function for dragging icon into container
- add homescreen function for storing apps' order
- fix bugs of container
+- add gauss blur component
diff --git a/public/index.html b/public/index.html
index 1010c03..256f79a 100644
--- a/public/index.html
+++ b/public/index.html
@@ -8,7 +8,7 @@
content="width=device-width, user-scalable=no, initial-scale=1.0"
/>
Star Web Components
-
+
diff --git a/public/manifest.webmanifest b/public/manifest.webmanifest
index 3f7056d..97b9938 100644
--- a/public/manifest.webmanifest
+++ b/public/manifest.webmanifest
@@ -21,6 +21,11 @@
"description": "StarWeb组件"
}
},
+ "permissions": {
+ "settings": {
+ "access": "readwrite"
+ }
+ },
"core": true,
"version": "0.0.1"
}
diff --git a/src/components/gauss_canvas/README.md b/src/components/gauss_canvas/README.md
new file mode 100644
index 0000000..c425bff
--- /dev/null
+++ b/src/components/gauss_canvas/README.md
@@ -0,0 +1,62 @@
+## 高斯模糊小组件
+
+小组件为解决用 `Css` 属性进行高斯模糊后,浏览器卡顿的问题。原理是用一个 `canvas` 将根据不同 `sigma` 值进行高斯模糊处理后的图展示出来。
+
+该组件的模糊算法当 `sigma` 越高时 ,其处理模糊的速度越快,越低时处理速度越慢,然而 `sigma` 越低,模糊程度越小,与原图越类似,可以用降低分辨率的方法替换模糊算法。因此组件设定了一个门槛值 `threshold` ,使用者可以根据机器性能和图片质量选择门槛值的高低。
+
+因为模糊算法耗时受 `sigma` 值和图片分辨率的影响,无法准确掌控,因此为了不阻塞主线程的渲染工作,模糊算法需要放入 `Web worker` 中运行。
+
+### 属性
+
+- `src`: 要高斯模糊的图片URL
+- `sigma`: 高斯模糊程度系数,除了第一次传值时不会有模糊渐变,之后传值时图片模糊会呈渐变,如若不需要动画则调用方法 `showImmediately` 并传值
+- `threshold`: 当 `sigma` 大于该值时,组件采用模糊算法,否则采用降低分辨率的方法模糊图片,默认值为1
+- `during`: 模糊渐变的最长时间,单位为 `ms`,默认值为 500
+- `bezier`: 模糊渐变的贝塞尔系数,接受参数为一个有四个数字元素的数组,默认值为 `[0.19, 1, 0.22, 1]`
+
+### 使用
+
+注意,请不要使用跨域图片资源,否则无法转化为 `ImageData` 进行模糊计算
+
+
+```html
+
+```
+
+```js
+import "@star-web-element/gauss"
+const canvas = document.querySelector('gauss-canvas');
+
+canvas.addEventListener('click', () => {
+ // 会有模糊渐变
+ canvas.sigma ^= 10
+})
+
+canvas.addEventListener('mousedown', handleEvent);
+canvas.addEventListener('mousemove', handleEvent);
+canvas.addEventListener('mouseup', handleEvent);
+
+let mouseData = {
+ start: 0,
+ moveDistance: 0,
+}
+function handleEvent(evt) {
+ switch (evt.type) {
+ case 'mousedown':
+ mouseData.start = evt.clientY;
+ break;
+ case ' mousemove':
+ if (mouseData.start) {
+ mouseData.moveDistance = evt.clientY - mouseData.start;
+ const conHeight = canvas.parentElement.offsetHeight;
+ const targetSigma = (mouseData.moveDistance / conHeight) * 10;
+ // 不会有模糊渐变
+ canvas.showImmediately(targetSigma);
+ }
+ break;
+ case 'mouseup':
+ mouseData.start = mouseData.moveDistance = 0;
+ break;
+ }
+}
+```
diff --git a/src/components/gauss_canvas/index.ts b/src/components/gauss_canvas/index.ts
new file mode 100644
index 0000000..23ce167
--- /dev/null
+++ b/src/components/gauss_canvas/index.ts
@@ -0,0 +1,287 @@
+import {html, css, LitElement} from 'lit'
+import {customElement, property, query} from 'lit/decorators.js'
+import workerUrl from './worker'
+enum ErrorType {
+ IMAGE_LOAD_FAILED = 'Image load failed!',
+ IMAGE_NOT_PREPARED = 'The Image resource is not completely prepared!',
+ IMAGE_CROS_ERROR = 'The Image resource crosses domain!',
+ CANVAS_PUTIMAGEDATA_ERROR = 'Canvas putImageData failed!',
+}
+
+const cubic_bezier = (
+ p0: number,
+ p1: number,
+ p2: number,
+ p3: number,
+ t: number
+): number => {
+ return (
+ p0 * Math.pow(1 - t, 3) +
+ 3 * p1 * t * Math.pow(1 - t, 2) +
+ 3 * p2 * t * t * (1 - t) +
+ p3 * Math.pow(t, 3)
+ )
+}
+
+@customElement('gauss-canvas')
+export default class GaussCanvas extends LitElement {
+ @query('#display') display!: HTMLCanvasElement
+ @property({type: String})
+ get src() {
+ return this._src
+ }
+ set src(value) {
+ if (value && value !== this._src) {
+ const loadCb = () => {
+ this._src = value
+ }
+ const errCb = () => {
+ this._img.src = this._src
+ this._img.removeEventListener('load', loadCb)
+ }
+
+ this._img.addEventListener('error', errCb, {once: true})
+ this._img.addEventListener('load', loadCb, {once: true})
+
+ this._img.src = value
+ }
+ }
+
+ @property() threshold: number = 1
+ @property() transition: boolean = false
+
+ @property()
+ get sigma() {
+ return this._sigma
+ }
+ set sigma(value) {
+ if (value < 0) {
+ value = 0
+ }
+ if (this._sigma == -1) {
+ this._sigma = Number(value)
+ this.show(value)
+ } else if (value !== this._sigma && this._targetSigma == -1) {
+ this._targetSigma = value
+ this.openAnimation()
+ } else {
+ console.info('changed target sigma')
+ }
+ }
+
+ @property({type: Number}) bezier: [number, number, number, number] = [
+ 0.19, 1, 0.22, 1,
+ ]
+ @property() during = 500
+
+ readonly _img: HTMLImageElement = document.createElement('img')
+ _loadStatus: boolean = false
+ _imgWidth: number = 0
+ _imgHeight: number = 0
+
+ readonly _trCanvas: HTMLCanvasElement = document.createElement('canvas')
+ readonly _trCtx = this._trCanvas.getContext('2d')!
+ _imageData: ImageData | undefined
+
+ _src: string = ''
+ _ctx!: CanvasRenderingContext2D
+ _sigma: number = -1
+ _targetSigma: number = -1
+
+ _worker: Worker | undefined
+ _id!: number
+
+ disWidth: number = 0
+ disHeight: number = 0
+
+ constructor(url: string) {
+ super()
+ this._img.addEventListener('error', this.loadError)
+ this._img.addEventListener('load', this.loadSuccess)
+ this._id = new Date().getTime()
+ this.src = url
+ }
+
+ loadError = () => {
+ this._loadStatus = false
+ this._imgWidth = this._imgHeight = 0
+
+ console.error(ErrorType.IMAGE_LOAD_FAILED)
+ }
+
+ loadSuccess = () => {
+ this._loadStatus = true
+ this._imgHeight = this._img.height
+ this._imgWidth = this._img.width
+ this.show(this.sigma)
+ }
+
+ drawWorker = (
+ sigma: number,
+ imageData: ImageData,
+ canvas: HTMLCanvasElement,
+ draw: Function
+ ) => {
+ if (sigma > this.threshold) {
+ if (!this._worker) {
+ this._worker = new Worker(workerUrl)
+ this._worker.addEventListener('message', (evt) => {
+ draw(evt.data.data, evt.data.id)
+ })
+ }
+
+ this._worker.postMessage({
+ imageData,
+ sigma,
+ id: this._id,
+ height: canvas.height,
+ width: canvas.width,
+ })
+ this.isFormatting = true
+ } else {
+ draw(imageData, this._id)
+ this.cb?.()
+ }
+ }
+
+ cb: Function | undefined
+ isFormatting: boolean = false
+ show = (sigma: number, cb?: Function) => {
+ this.cb = cb
+ return new Promise((res, rej) => {
+ if (!this._loadStatus) {
+ if (!this._img.src.includes(this._src)) {
+ this._img.src = this._src
+ }
+
+ return
+ }
+
+ if (this.isFormatting) {
+ return res(void 0)
+ }
+ let ratio: number = sigma
+ if (sigma == 0) {
+ ratio = 1
+ } else if (sigma < this.threshold) {
+ ratio = 2 * this.threshold - 1
+ }
+
+ const canvas = document.createElement('canvas')
+ const ctx = canvas.getContext('2d')!
+ canvas.height = this._img.height / ratio
+ canvas.width = this._img.width / ratio
+
+ ctx.drawImage(
+ this._img,
+ 0,
+ 0,
+ this._img.width,
+ this._img.height,
+ 0,
+ 0,
+ canvas.width,
+ canvas.height
+ )
+
+ const draw = (imageData: ImageData, dataId: number) => {
+ if (dataId == this._id) {
+ if (this.cb) {
+ this.cb?.()
+ }
+ // +100 为了清除拖影
+ this.disHeight = this.display.height = imageData.height /* + 100 */
+ this.disWidth = this.display.width = imageData.width
+ this._ctx.putImageData(imageData, 0, 0)
+ this.isFormatting = false
+ res(void 0)
+ }
+ }
+ try {
+ let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
+ this.drawWorker(sigma, imageData, canvas, draw)
+ } catch (error: any) {
+ if (error.code == 18) {
+ return rej(ErrorType.IMAGE_CROS_ERROR)
+ }
+ return rej(ErrorType.CANVAS_PUTIMAGEDATA_ERROR)
+ }
+ })
+ }
+
+ showImmediately(sigma: number) {
+ return new Promise((res) => {
+ this._sigma = sigma
+ this.show(sigma).then(res)
+ })
+ }
+
+ _startTime!: number
+ animation: Function | undefined
+ openAnimation() {
+ this._startTime = new Date().getTime()
+ const getCurSigma = () => {
+ const t = (new Date().getTime() - this._startTime) / this.during
+ let ratio = cubic_bezier(...this.bezier, t)
+
+ if (ratio > 0.95) {
+ return this._targetSigma
+ }
+ const result = (this._targetSigma - this._sigma) * ratio + this._sigma
+
+ return result > 0 ? result : 0
+ }
+ let changeSigma = () => {
+ this._sigma = getCurSigma()
+
+ if (this._targetSigma !== this._sigma) {
+ return true
+ } else {
+ return false
+ }
+ }
+ this.animation = () => {
+ if (changeSigma()) {
+ // console.time(`sigma: ${this._sigma}`)
+ this.show(this._sigma, () => {
+ // console.timeEnd(`sigma: ${this._sigma}`)
+ // @ts-ignore
+ requestAnimationFrame(this.animation!)
+ })
+ } else {
+ this._targetSigma = -1
+ }
+ }
+
+ this.animation()
+ }
+
+ protected firstUpdated() {
+ this._ctx = this.display.getContext('2d')!
+ }
+
+ render() {
+ return html`
+
+ `
+ }
+
+ static styles = css`
+ :host {
+ display: block;
+ overflow: hidden;
+ }
+ canvas {
+ display: block;
+ margin: auto;
+ height: calc(100%);
+ width: 100%;
+ }
+ `
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gauss-canvas': GaussCanvas
+ }
+}
diff --git a/src/components/gauss_canvas/package.json b/src/components/gauss_canvas/package.json
new file mode 100644
index 0000000..f852ed3
--- /dev/null
+++ b/src/components/gauss_canvas/package.json
@@ -0,0 +1,25 @@
+{
+ "name": "@star-web-components/gauss",
+ "version": "0.0.1",
+ "description": "",
+ "type": "module",
+ "main": "./index.js",
+ "module": "./index.js",
+ "exports": {
+ ".": {
+ "default": "./index.js"
+ },
+ "./index": {
+ "default": "./index.js"
+ },
+ "./index.js": {
+ "default": "./index.js"
+ },
+ "./icons/index.js": {
+ "default": "./icons/index.js"
+ },
+ "./package.json": "./package.json"
+ },
+ "author": "",
+ "license": "ISC"
+}
diff --git a/src/components/gauss_canvas/worker.ts b/src/components/gauss_canvas/worker.ts
new file mode 100644
index 0000000..978be46
--- /dev/null
+++ b/src/components/gauss_canvas/worker.ts
@@ -0,0 +1,181 @@
+// import {
+// cubic_bezier,
+// fastBlur,
+// genSrcAndDest,
+// mergeChannels,
+// genKernelsForGaussian,
+// _fastBlur,
+// hFastMotionBlur,
+// vFastMotionBlur,
+// } from './utils'
+// @ts-ignore
+const workCode = () => {
+ function genKernelsForGaussian(sigma: number, n: number) {
+ const wIdeal = Math.sqrt((12 * Math.pow(sigma, 2)) / n + 1)
+ const sizes = []
+ let wl = Math.floor(wIdeal)
+
+ if (wl % 2 === 0) {
+ wl--
+ }
+ const wu = wl + 2
+ let m =
+ (12 * Math.pow(sigma, 2) - n * Math.pow(wl, 2) - 4 * n * wl - 3 * n) /
+ (-4 * wl - 4)
+ m = Math.round(m)
+
+ for (let i = 0; i < n; i++) {
+ sizes.push(i < m ? wl : wu)
+ }
+
+ return sizes
+ }
+
+ function genSrcAndDest(data: Uint8ClampedArray) {
+ const dataInt8 = new Uint8ClampedArray(data.buffer)
+ const simpleChannelLength = dataInt8.length / 4
+ const r = new Uint8ClampedArray(simpleChannelLength)
+ const g = new Uint8ClampedArray(simpleChannelLength)
+ const b = new Uint8ClampedArray(simpleChannelLength)
+ const a = new Uint8ClampedArray(simpleChannelLength)
+ const _r = new Uint8ClampedArray(simpleChannelLength)
+ const _g = new Uint8ClampedArray(simpleChannelLength)
+ const _b = new Uint8ClampedArray(simpleChannelLength)
+ const _a = new Uint8ClampedArray(simpleChannelLength)
+ for (let i = 0; i < simpleChannelLength; i++) {
+ _r[i] = r[i] = dataInt8[i * 4]
+ _g[i] = g[i] = dataInt8[i * 4 + 1]
+ _b[i] = b[i] = dataInt8[i * 4 + 2]
+ _a[i] = a[i] = dataInt8[i * 4 + 3]
+ }
+ return {src: [r, g, b, a], dest: [_r, _g, _b, _a]}
+ }
+
+ function mergeChannels([r, g, b, a]: Uint8ClampedArray[]) {
+ const simpleChannelLength = r.length
+ const data = new Uint8ClampedArray(simpleChannelLength * 4)
+ for (let i = 0; i < simpleChannelLength; i++) {
+ data[4 * i] = r[i]
+ data[4 * i + 1] = g[i]
+ data[4 * i + 2] = b[i]
+ data[4 * i + 3] = a[i]
+ }
+ return data
+ }
+
+ function hFastMotionBlur(
+ src: Uint8ClampedArray,
+ dest: Uint8ClampedArray,
+ width: number,
+ height: number,
+ radius: number
+ ) {
+ for (let i = 0; i < height; i++) {
+ let accumulation = radius * src[i * width]
+ for (let j = 0; j <= radius; j++) {
+ accumulation += src[i * width + j]
+ }
+
+ dest[i * width] = Math.round(accumulation / (2 * radius + 1))
+
+ for (let j = 1; j < width; j++) {
+ const left = Math.max(0, j - radius - 1)
+ const right = Math.min(width - 1, j + radius)
+ accumulation =
+ accumulation + (src[i * width + right] - src[i * width + left])
+ dest[i * width + j] = Math.round(accumulation / (2 * radius + 1))
+ }
+ }
+ }
+
+ function vFastMotionBlur(
+ src: Uint8ClampedArray,
+ dest: Uint8ClampedArray,
+ width: number,
+ height: number,
+ radius: number
+ ) {
+ for (let i = 0; i < width; i++) {
+ let accumulation = radius * src[i]
+ for (let j = 0; j <= radius; j++) {
+ accumulation += src[j * width + i]
+ }
+
+ dest[i] = Math.round(accumulation / (2 * radius + 1))
+
+ for (let j = 1; j < height; j++) {
+ const top = Math.max(0, j - radius - 1)
+ const bottom = Math.min(height - 1, j + radius)
+ accumulation =
+ accumulation + src[bottom * width + i] - src[top * width + i]
+ dest[j * width + i] = Math.round(accumulation / (2 * radius + 1))
+ }
+ }
+ }
+
+ function _fastBlur(
+ src: Uint8ClampedArray,
+ dest: Uint8ClampedArray,
+ width: number,
+ height: number,
+ radius: number
+ ) {
+ hFastMotionBlur(dest, src, width, height, radius)
+ vFastMotionBlur(src, dest, width, height, radius)
+ }
+
+ function fastBlur(
+ src: Uint8ClampedArray,
+ dest: Uint8ClampedArray,
+ width: number,
+ height: number,
+ sigma: number
+ ) {
+ const boxes = genKernelsForGaussian(sigma, 3)
+
+ for (let i = 0; i < src.length; i++) {
+ dest[i] = src[i]
+ }
+
+ _fastBlur(src, dest, width, height, (boxes[0] - 1) / 2)
+ _fastBlur(src, dest, width, height, (boxes[1] - 1) / 2)
+ _fastBlur(src, dest, width, height, (boxes[2] - 1) / 2)
+
+ return dest
+ }
+
+ onmessage = (evt) => {
+ postMessage({data: blurCanvas(evt.data), id: evt.data.id})
+ }
+ let blurCanvas = ({
+ imageData,
+ height,
+ width,
+ sigma,
+ }: {
+ imageData: ImageData
+ sigma: number
+ height: number
+ width: number
+ }) => {
+ const {src: srcRgba, dest: destRgba} = genSrcAndDest(imageData.data)
+
+ for (let i = 0; i < 3; i++) {
+ fastBlur(srcRgba[i], destRgba[i], width, height, sigma)
+ }
+ const destData = mergeChannels(destRgba)
+ imageData.data.set(destData)
+ return imageData
+ }
+}
+
+const transfer = () => {
+ let codeStr: string = ''
+
+ codeStr += `(${workCode.toString()})()`
+ return codeStr
+}
+
+let workBlob = new Blob([transfer()])
+
+export default URL.createObjectURL(workBlob)
diff --git a/src/test/panels/activeoverlay/activeoverlay.ts b/src/test/panels/activeoverlay/activeoverlay.ts
index 42be2cc..65bd198 100644
--- a/src/test/panels/activeoverlay/activeoverlay.ts
+++ b/src/test/panels/activeoverlay/activeoverlay.ts
@@ -1,5 +1,5 @@
import {html, LitElement, css} from 'lit'
-import {customElement, property} from 'lit/decorators.js'
+import {customElement} from 'lit/decorators.js'
import '../../../components/button/button'
import '../../../components/overlay/active-overlay'
import {OverlayStack} from '../../../components/overlay/overlay-stack'
diff --git a/src/test/panels/digicipher/digicipher.ts b/src/test/panels/digicipher/digicipher.ts
index 2014cf8..34b61a9 100644
--- a/src/test/panels/digicipher/digicipher.ts
+++ b/src/test/panels/digicipher/digicipher.ts
@@ -1,5 +1,5 @@
import {html, LitElement, css} from 'lit'
-import {customElement, property} from 'lit/decorators.js'
+import {customElement} from 'lit/decorators.js'
import '../icon/icon'
@customElement('panel-digicipher')
diff --git a/src/test/panels/gauss_canvas/gauss-blur.ts b/src/test/panels/gauss_canvas/gauss-blur.ts
new file mode 100644
index 0000000..34b9ddc
--- /dev/null
+++ b/src/test/panels/gauss_canvas/gauss-blur.ts
@@ -0,0 +1,127 @@
+import {html, css, LitElement, CSSResultGroup} from 'lit'
+import {customElement, property, query} from 'lit/decorators.js'
+import '../../../components/gauss_canvas/index'
+import GaussCanvas from '../../../components/gauss_canvas/index'
+
+@customElement('panel-gauss')
+export class GaussBlur extends LitElement {
+ @query('#container') container!: HTMLDivElement
+ @query('gauss-canvas') canvas!: GaussCanvas
+ // _src = '/src/test/panels/gauss_canvas/big.jpeg'
+ @property() _src!: string
+ // 'https://fanyiapp.cdn.bcebos.com/cms/image/cfacf96e5beb2a8444e016b96fb96ab6.jpg'
+ @property({type: Number}) sigma: number = 2
+ @property({type: Number}) topPositionSigma: number = 20
+ @property({type: Number}) bottomPositionSigma: number = 2
+
+ _moveFlag: boolean = false
+ _offsetY: number = 0
+ _start: number = 0
+ _moveDistance: number = 0
+ set offsetY(value: number) {
+ this._offsetY = -value
+ this.canvas.display.style.transform = `translateY(${value}px)`
+ }
+
+ handleEvent = (evt: TouchEvent | MouseEvent) => {
+ switch (evt.type) {
+ case 'touchstart':
+ case 'mousedown':
+ this._moveFlag = false
+ if (evt instanceof MouseEvent) {
+ this._start = evt.clientY
+ } else {
+ this._start = evt.touches[0].pageY
+ }
+ break
+ case 'touchmove':
+ case 'mousemove':
+ if (this._start) {
+ if (evt instanceof MouseEvent) {
+ this._moveDistance = evt.clientY - this._start
+ } else {
+ this._moveDistance = evt.touches[0].pageY - this._start
+ }
+
+ if (this._moveDistance > 0) this._moveDistance = 0
+
+ requestAnimationFrame(() => {
+ this.offsetY = this._moveDistance
+ this.changeSigma()
+ })
+ }
+ this._moveFlag = true
+
+ break
+ case 'touchend':
+ case 'mouseup':
+ this._start = this._moveDistance = 0
+ break
+ case 'click':
+ !this._moveFlag && (this.canvas.sigma ^= 10)
+ break
+ }
+ }
+
+ /**
+ * change sigma fowllowing this.offsetY
+ */
+ changeSigma() {
+ const conHeight = this.container.offsetHeight
+ const targetSigma =
+ (this._offsetY / conHeight) *
+ (this.topPositionSigma - this.bottomPositionSigma) +
+ this.bottomPositionSigma
+ this.canvas.showImmediately(targetSigma)
+ }
+
+ firstUpdated() {
+ ;(window as any).panel = this
+
+ this.addEventListener('touchstart', this)
+ this.addEventListener('touchmove', this)
+ this.addEventListener('touchend', this)
+ this.addEventListener('mousedown', this)
+ this.addEventListener('mousemove', this)
+ this.addEventListener('mouseup', this)
+ this.canvas.addEventListener('click', this)
+ }
+ handleInputFile(evt: Event) {
+ const imgfile = (evt.target as HTMLInputElement).files?.[0]
+ if (imgfile) {
+ this._src = URL.createObjectURL(imgfile)
+ }
+ }
+ render() {
+ return html`
+
+
+
+
+ `
+ }
+
+ static styles?: CSSResultGroup | undefined = css`
+ #container {
+ height: 100vh;
+ width: 100vw;
+ will-change: transform;
+ overflow: hidden;
+ }
+ input {
+ position: absolute;
+ top: 0;
+ left: 0;
+ }
+ gauss-canvas {
+ height: 100vh;
+ width: 100vw;
+ }
+ `
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'panel-gauss': GaussBlur
+ }
+}
diff --git a/src/test/panels/overflowmenu/overflowmenu.ts b/src/test/panels/overflowmenu/overflowmenu.ts
index bc27d84..267004e 100644
--- a/src/test/panels/overflowmenu/overflowmenu.ts
+++ b/src/test/panels/overflowmenu/overflowmenu.ts
@@ -1,5 +1,5 @@
import {html, LitElement, css, CSSResultArray} from 'lit'
-import {customElement, property} from 'lit/decorators.js'
+import {customElement} from 'lit/decorators.js'
import '../../../components/button/button'
import '../../../components/ul/ul'
import '../../../components/li//li'
diff --git a/src/test/panels/root.ts b/src/test/panels/root.ts
index f8a6115..1bef929 100644
--- a/src/test/panels/root.ts
+++ b/src/test/panels/root.ts
@@ -14,6 +14,7 @@ import './card/card'
import './indicators/indicators'
import './indicators/home-indicator'
import './blur/use-blur'
+import './gauss_canvas/gauss-blur'
import './button/button'
import './container/container'
import './radio/radio'
@@ -249,6 +250,14 @@ export class PanelRoot extends LitElement {
href="#blur"
>
+
+