From 6de6aa0accea121be226af521360b82f2d99849d Mon Sep 17 00:00:00 2001 From: Rudis Muiznieks Date: Sat, 9 May 2020 14:39:27 -0500 Subject: [PATCH] added FilmGrain effect --- README.md | 22 ++++++++- src/FilmGrain.ts | 116 +++++++++++++++++++++++++++++++++++++++++++++++ src/Spooky.ts | 2 - src/main.ts | 4 +- 4 files changed, 140 insertions(+), 4 deletions(-) create mode 100644 src/FilmGrain.ts delete mode 100644 src/Spooky.ts diff --git a/README.md b/README.md index ab59152..4c07b50 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,22 @@ # spooky.js -Javascript/CSS effects to make a website spooky. + +## Available Effects + +### Film Grain + +Based on original code from [Szenia Zadvornykh](https://codepen.io/zadvorsky/pen/PwyoMm). + +``` +var fg = new Spooky.FilmGrain(options); +fg.execute(); +fg.stop(); +``` + +Takes optional `options` configuration object: + - `patternSize`: dimensions of the repeating grain pattern (`64`) + - `alpha`: transparency of the effect from 0-255 (`25`) + - `grainScale`: x and y scale of each grain (`{x: 3, y: 1}`) + - `refreshInterval`: how often to redraw the effect + - `canvasId`: optional id of an existing canvas to apply the effect to + +If no existing `canvasId` is provided then the effect will be applied to the entire browser screen. diff --git a/src/FilmGrain.ts b/src/FilmGrain.ts new file mode 100644 index 0000000..95687f8 --- /dev/null +++ b/src/FilmGrain.ts @@ -0,0 +1,116 @@ +// credit: https://codepen.io/zadvorsky/pen/PwyoMm + +import * as $ from 'jquery'; + +export interface FilmGrainOptions { + patternSize?: number; + alpha?: number; + grainScale?: { x: number, y: number }; + refreshInterval?: number; + canvasId?: string; +} + +export class FilmGrain { + private patternSize: number; + private grainScaleX: number; + private grainScaleY: number; + private refreshInterval: number; + private alpha: number; + private canvasId?: string; + private patternPixelDataLength: number; + + private canvas?: JQuery; + private patternCanvas?: HTMLCanvasElement; + + private viewWidth: number = 0; + private viewHeight: number = 0; + private context?: CanvasRenderingContext2D; + private patternContext?: CanvasRenderingContext2D; + private patternData?: ImageData; + + private frame: number = 0; + + constructor(options?: FilmGrainOptions) { + this.patternSize = options?.patternSize || 64; + this.grainScaleX = options?.grainScale?.x || 3; + this.grainScaleY = options?.grainScale?.y || 1; + this.refreshInterval = options?.refreshInterval || 4; + this.alpha = options?.alpha || 25; + this.canvasId = options?.canvasId; + this.patternPixelDataLength = this.patternSize * this.patternSize * 4; + } + + private initCanvas(canvasId?: string): void { + if (!this.canvasId) { + // create full-screne canvas + this.canvas = $('') + .css({ + position: 'fixed', + top: '0', + left: '0', + width: '100%', + height: '100%', + pointerEvents: 'none', + }) + .click(() => false); + $('html').css({ + position: 'relative', + width: '100%', + height: '100%', + }).append(this.canvas); + } else { + // use provided canvas + this.canvas = $(`#${this.canvasId}`); + } + + this.viewWidth = this.canvas[0].width = this.canvas[0].clientWidth; + this.viewHeight = this.canvas[0].height = this.canvas[0].clientHeight; + this.context = this.canvas[0].getContext('2d')!; + this.context.scale(this.grainScaleX, this.grainScaleY); + } + + private initGrain(): void { + this.patternCanvas = document.createElement('canvas'); + this.patternCanvas.width = this.patternSize; + this.patternCanvas.height = this.patternSize; + this.patternContext = this.patternCanvas.getContext('2d')!; + this.patternData = this.patternContext.createImageData( + this.patternSize, this.patternSize); + } + + private update(): void { + // put a random shade of gray into every pixel of the pattern + if (this.patternData && this.patternContext) { + for (let i = 0; i < this.patternPixelDataLength; i += 4) { + const value = (Math.random() * 255) | 0; + this.patternData.data[i] = value; + this.patternData.data[i + 1] = value; + this.patternData.data[i + 2] = value; + this.patternData.data[i + 3] = this.alpha; + } + this.patternContext.putImageData(this.patternData, 0, 0); + } + } + + private draw(): void { + if (this.context && this.patternCanvas) { + this.context.clearRect(0, 0, this.viewWidth, this.viewHeight); + this.context.fillStyle = this.context.createPattern(this.patternCanvas, 'repeat')!; + this.context.fillRect(0, 0, this.viewWidth, this.viewHeight); + } + } + + private loop(): void { + if (++(this.frame) % this.refreshInterval === 0) { + this.update(); + this.draw(); + } + requestAnimationFrame(() => this.loop()); + } + + public execute(): void { + this.initCanvas(); + this.initGrain(); + requestAnimationFrame(() => this.loop()); + } +} diff --git a/src/Spooky.ts b/src/Spooky.ts deleted file mode 100644 index af37025..0000000 --- a/src/Spooky.ts +++ /dev/null @@ -1,2 +0,0 @@ -export class Spooky { -} diff --git a/src/main.ts b/src/main.ts index 8690c08..5a93d83 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1 +1,3 @@ -module.exports = require('./Spooky.js').Spooky; +module.exports = { + FilmGrain: require('./FilmGrain.js').FilmGrain, +}