├── .babelrc ├── .editorconfig ├── .gitignore ├── README.md ├── assets ├── demo.gif └── distortion-text.gif ├── index.html ├── package.json ├── rollup.config.js ├── src ├── DistortionText.js ├── FliesText.js ├── LiquidDistortionText.js ├── SpitColorChannelText.js ├── createBlotterComponent.js ├── hasBlotterInstance.js ├── index.js └── materials.js ├── website.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false 5 | }], 6 | "stage-0", 7 | "react" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # See https://help.github.com/ignore-files/ for more about ignoring files. 3 | 4 | # dependencies 5 | node_modules 6 | 7 | # builds 8 | build 9 | dist 10 | .rpt2_cache 11 | .cache 12 | 13 | # misc 14 | .DS_Store 15 | .env 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | website.js 26 | index.html -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-text-fun 2 | 3 | > React meets Blotter.js 4 | 5 |

6 | 7 |

8 | 9 | ## Table of contents 10 | 11 | * [Introduction](#introduction) 12 | 13 | * [Install](#install) 14 | 15 | * [Example](#example) 16 | 17 | * [Components](#blotter-components) 18 | 19 | * [Styling text](#styling-text) 20 | 21 | * [Using text canvas](#using-text-canvas) 22 | 23 | * [Live examples](#live-examples) 24 | 25 | ## Introdution 26 | 27 | `react-text-fun` is a small component library that encapsulates [Blotter.js](https://blotter.js.org/#/materials) shader materials in the form of React components and provides a very easy to use API. 28 | 29 | I created `react-text-fun` after finding myself imperatively using the Blotter.js APIs for custom and existing materials. I decided to convert all its shader materials in the form of React components to make it easier to work with. 30 | 31 | Hope you find it useful as well 🙂 32 | 33 | ## Install 34 | 35 | ``` 36 | yarn add react-text-fun 37 | ``` 38 | 39 | This package also depends on `Blotter.js` so make sure you put the below script in your HTML file. 40 | 41 | ``` 42 | 43 | ``` 44 | 45 | ## Example 46 | 47 | Let's take an example of distortion text material that distorts the shape of the text using various transforms 48 | 49 | ```jsx 50 | import { DistortionText } from 'react-text-fun' 51 | import React from 'react'; 52 | import ReactDOM from 'react-dom'; 53 | 54 | const App = props => ( 55 |
56 | 57 |
58 | ) 59 | 60 | // Assuming you have an element with id 'root' to which you want the component to render to. 61 | ReactDOM.render(, document.getElementById('root')) 62 | ``` 63 | 64 | If your example compiles successfully, you should see this output. 65 | 66 |

67 | 68 |

69 | 70 | Cool, isn't it? 71 | 72 | [Check out the API reference](#distortion-text) for `DistortionText` component to see what other effects you can apply to the text. 73 | 74 | ## Blotter components 75 | 76 | ### Distortion Text 77 | 78 | Distortion text is based on the [Rolling Distort Material](https://blotter.js.org/#/materials/RollingDistortMaterial) in Blotter.js. 79 | 80 | **Example** 81 | 82 | ```jsx 83 | import { DistortionText } from 'react-text-fun' 84 | 85 | 95 | ``` 96 | 97 | | Prop | Description | Type | 98 | | ------------- |:-------------:| -----:| 99 | | `speed` | Increase or decrease the speed of animation applied to the distortion on your text | number | 100 | | `rotation` | Change the rotation of distortion effect | number | 101 | | `distortX` | update the horizontal position in which the distortion effect will be applied | number | 102 | | `distortY` | update the vertical position in which the distortion effect will be applied | number | 103 | | `noiseAmplitude` | change the noise amplitude (amplitude of toughs and crests) | number | 104 | | `noiseVolatility` | describes the overall change your text will experience | number | 105 | 106 | I'll recommend reading [this](https://pudding.cool/2018/02/waveforms/) brilliant piece written by [Josh Comeau](https://www.joshwcomeau.com/) on Waveforms. It will give you a little more idea on how and what values you should use to update the noise amplitude, and change its volatility. 107 | 108 | ### Flies Text 109 | 110 | Flies Text component is based on the [FliesMaterial](https://blotter.js.org/#/materials/FliesMaterial) in Blotter.js 111 | 112 | ```jsx 113 | import { FliesText } from 'react-text-fun'; 114 | 115 | 125 | ``` 126 | 127 | | Prop | Description | Type | 128 | | ------------- |:-------------:| -----:| 129 | | `cellWidth` | Width of a cell | number | 130 | | `cellRadius` | Radius of a cell | number | 131 | | `speed` | Animation speed | number | 132 | | `dodge` | whether or not to dodge cells from a position (x-axis or y-axis) | boolean | 133 | | `dodgeX` | dodge position of cells around x-axis | number | 134 | | `dodgeY` | dodge position of cells around y-axis | number | 135 | 136 | ### Split color channel 137 | 138 | Split color channel is based on [ChannelSplitMaterial](https://blotter.js.org/#/materials/ChannelSplitMaterial) in Blotter.js 139 | 140 | ```jsx 141 | import { SplitColorChannelText } from 'react-text-fun'; 142 | 143 | 151 | ``` 152 | 153 | | Prop | Description | Type | 154 | | ------------- |:-------------:| -----:| 155 | | `rotation` | Change the angle of rgb channel splitting | number | 156 | | `rgbOffset` | Describes the distance apart the RGB values should spread | number | 157 | | `addBlur` | Add blur to the text | boolean | 158 | | `addNoise` | Add noise distortion to text | boolean | 159 | 160 | ### Liquid distortion text 161 | 162 | ```jsx 163 | import { LiquidDistortionText } from 'react-text-fun'; 164 | 165 | 171 | ``` 172 | 173 | | Prop | Description | Type | 174 | | ------------- |:-------------:| -----:| 175 | | `speed` | Speed of the animation | number | 176 | | `volatility` | Describes the change in distortion of a text | number | 177 | 178 | ## Styling text 179 | 180 | You can use the below props with any of the above component to style the text. These are the common props. 181 | 182 | | Prop | Description | Type | 183 | | ------------- |:-------------:| -----:| 184 | | `id` | An unique id for the canvas | string | 185 | | `appendTo` | Specify an id for an element to which the canvas should be appended | string | 186 | | `text` | Text string to render | string | 187 | | `fontFamily` | Set a different font type | string | 188 | | `fontSize` | Set the font size | number | 189 | | `fontWeight` | Set the font weight | number | 190 | | `fill` | Sets the text color | string | 191 | | `fontStyle` | Specify a font style (italic, normal or bold) | string | 192 | | `lineHeight` | Set the line height | number | 193 | | `paddingTop` | Apply top padding | number | 194 | | `paddingBottom` | Apply bottom padding | number | 195 | | `paddingLeft` | Apply padding on left side | number | 196 | | `paddingRight` | Apply padding on right side | number | 197 | 198 | ## Using text canvas 199 | 200 | You can also access the canvas which renders the text using the callback function `get2dContext`. As the prop name suggests, the callback function receives the 2D rendering context for the drawing surface as an argument. This is useful if you want to update the canvas using any other third party library. 201 | 202 | `get2dContext` can be used with any of the above material components. For instance, here is an example of how you would use it with `FliesText` component. 203 | 204 | ```jsx 205 | console.log(ctx)} /> 206 | ``` 207 | 208 | ## Live examples 209 | 210 | You can find the live code examples for all the components on the [codesandbox](https://codesandbox.io/embed/9jvp8n69kw) -------------------------------------------------------------------------------- /assets/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitin42/react-text-fun/19faffb9da78d4f43afb3bb474e5f0ad50dfe965/assets/demo.gif -------------------------------------------------------------------------------- /assets/distortion-text.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitin42/react-text-fun/19faffb9da78d4f43afb3bb474e5f0ad50dfe965/assets/distortion-text.gif -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | 12 | react-text-fun 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-text-fun", 3 | "version": "1.0.0", 4 | "description": "React meets Blotter.js", 5 | "author": "nitin42", 6 | "license": "MIT", 7 | "repository": "nitin42/react-text-fun", 8 | "main": "dist/index.js", 9 | "module": "dist/index.es.js", 10 | "jsnext:main": "dist/index.es.js", 11 | "engines": { 12 | "node": ">=8", 13 | "npm": ">=5" 14 | }, 15 | "scripts": { 16 | "build": "rollup -c", 17 | "start": "rollup -c -w", 18 | "example": "parcel index.html", 19 | "build:website": "parcel build website.js" 20 | }, 21 | "peerDependencies": { 22 | "react": "^15.0.0 || ^16.0.0", 23 | "react-dom": "^15.0.0 || ^16.0.0" 24 | }, 25 | "devDependencies": { 26 | "@svgr/rollup": "^2.4.1", 27 | "babel-core": "^6.26.3", 28 | "babel-plugin-external-helpers": "^6.22.0", 29 | "babel-preset-env": "^1.7.0", 30 | "babel-preset-react": "^6.24.1", 31 | "babel-preset-stage-0": "^6.24.1", 32 | "cross-env": "^5.1.4", 33 | "randomcolor": "^0.5.4", 34 | "react": "^15.0.0 || ^16.0.0", 35 | "react-dom": "^15.0.0 || ^16.0.0", 36 | "rollup": "^0.64.1", 37 | "rollup-plugin-babel": "^3.0.7", 38 | "rollup-plugin-commonjs": "^9.1.3", 39 | "rollup-plugin-node-resolve": "^3.3.0", 40 | "rollup-plugin-peer-deps-external": "^2.2.0", 41 | "rollup-plugin-postcss": "^1.6.2", 42 | "rollup-plugin-url": "^1.4.0" 43 | }, 44 | "files": ["dist"], 45 | "dependencies": { 46 | "@emotion/core": "^10.0.10", 47 | "@emotion/styled": "^10.0.11", 48 | "parcel": "^1.12.3", 49 | "react-input-range": "^1.3.0", 50 | "use-mobile-detect-hook": "^1.0.2" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel' 2 | import commonjs from 'rollup-plugin-commonjs' 3 | import external from 'rollup-plugin-peer-deps-external' 4 | import postcss from 'rollup-plugin-postcss' 5 | import resolve from 'rollup-plugin-node-resolve' 6 | import url from 'rollup-plugin-url' 7 | import svgr from '@svgr/rollup' 8 | 9 | import pkg from './package.json' 10 | 11 | export default { 12 | input: 'src/index.js', 13 | output: [ 14 | { 15 | file: pkg.main, 16 | format: 'cjs', 17 | sourcemap: true 18 | }, 19 | { 20 | file: pkg.module, 21 | format: 'es', 22 | sourcemap: true 23 | } 24 | ], 25 | plugins: [ 26 | external(), 27 | postcss({ 28 | modules: true 29 | }), 30 | url(), 31 | svgr(), 32 | babel({ 33 | exclude: 'node_modules/**', 34 | plugins: [ 'external-helpers' ] 35 | }), 36 | resolve(), 37 | commonjs() 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /src/DistortionText.js: -------------------------------------------------------------------------------- 1 | import { createBlotterComponent } from './createBlotterComponent'; 2 | import { distortionText } from './materials'; 3 | 4 | export const DistortionText = createBlotterComponent({ 5 | material: distortionText, 6 | defaultProps: { 7 | id: 'distortion-text-component', 8 | text: 'Hello World', 9 | fontFamily: 'sans-serif', 10 | fontSize: 45, 11 | fontWeight: 400, 12 | rotation: 0.0, 13 | rgbOffset: 0.05, 14 | fill: '#4f4f4f', 15 | fontStyle: 'normal', 16 | paddingBottom: 0, 17 | paddingTop: 0, 18 | paddingRight: 0, 19 | paddingLeft: 0, 20 | speed: 0.084, 21 | rotation: 120.0, 22 | distortX: 0.06, 23 | distortY: 0.09, 24 | noiseAmplitude: 0.101, 25 | noiseVolatility: 8 26 | }, 27 | setMaterialValues: (material, props) => { 28 | material.uniforms.uNoiseDistortVolatility.value = props.noiseVolatility; 29 | material.uniforms.uNoiseDistortAmplitude.value = props.noiseAmplitude; 30 | material.uniforms.uDistortPosition.value = [props.distortX, props.distortY]; 31 | material.uniforms.uRotation.value = props.rotation; 32 | material.uniforms.uSpeed.value = props.speed; 33 | material.uniforms.uSineDistortSpread.value = 0; 34 | material.uniforms.uSineDistortCycleCount.value = 0; 35 | material.uniforms.uSineDistortAmplitude.value = 0; 36 | }, 37 | displayName: 'DistortionText' 38 | }); 39 | -------------------------------------------------------------------------------- /src/FliesText.js: -------------------------------------------------------------------------------- 1 | import { fliesMaterial } from './materials'; 2 | import { createBlotterComponent } from './createBlotterComponent'; 3 | 4 | export const FliesText = createBlotterComponent({ 5 | material: fliesMaterial, 6 | defaultProps: { 7 | cellWidth: 0.04, 8 | cellRadius: 0.5, 9 | speed: 2.0, 10 | dodge: false, 11 | dodgeX: 0.5, 12 | dodgeY: 0.8, 13 | dodgeSpread: 0.75, 14 | id: 'flies-text-component', 15 | text: 'Hello World', 16 | fontFamily: 'sans-serif', 17 | fontSize: 45, 18 | fontWeight: 400, 19 | fill: '#4f4f4f', 20 | fontStyle: 'normal', 21 | paddingBottom: 0, 22 | paddingTop: 0, 23 | paddingRight: 0, 24 | paddingLeft: 0, 25 | lineHeight: 1.5 26 | }, 27 | displayName: 'FliesText', 28 | setMaterialValues: (material, props) => { 29 | material.uniforms.uPointCellWidth.value = parseFloat(props.cellWidth); 30 | material.uniforms.uPointRadius.value = parseFloat(props.cellRadius); 31 | material.uniforms.uSpeed.value = parseFloat(props.speed); 32 | material.uniforms.uDodge.value = props.dodge ? 1.0 : 0.0; 33 | material.uniforms.uDodgePosition.value = [ 34 | parseFloat(props.dodgeX), 35 | parseFloat(props.dodgeY) 36 | ]; 37 | material.uniforms.uDodgeSpread.value = parseFloat(props.dodgeSpread); 38 | } 39 | }); 40 | -------------------------------------------------------------------------------- /src/LiquidDistortionText.js: -------------------------------------------------------------------------------- 1 | import { createBlotterComponent } from './createBlotterComponent'; 2 | import { liquidDistortionMaterial } from './materials'; 3 | 4 | export const LiquidDistortionText = createBlotterComponent({ 5 | defaultProps: { 6 | id: 'liquid-distortion-component', 7 | text: 'Hello World', 8 | fontFamily: 'sans-serif', 9 | fontSize: 45, 10 | fontWeight: 400, 11 | fill: '#4f4f4f', 12 | fontStyle: 'normal', 13 | paddingBottom: 0, 14 | paddingTop: 0, 15 | paddingRight: 0, 16 | paddingLeft: 0, 17 | lineHeight: 1.5, 18 | speed: 1.5, 19 | volatility: 0.04 20 | }, 21 | displayName: 'LiquidDistortionText', 22 | setMaterialValues: (material, props) => { 23 | material.uniforms.uSpeed.value = parseFloat(props.speed); 24 | material.uniforms.uVolatility.value = parseFloat(props.volatility); 25 | }, 26 | material: liquidDistortionMaterial 27 | }); 28 | -------------------------------------------------------------------------------- /src/SpitColorChannelText.js: -------------------------------------------------------------------------------- 1 | import { channelSplitMaterial } from './materials'; 2 | 3 | import { createBlotterComponent } from './createBlotterComponent'; 4 | 5 | export const SplitColorChannelText = createBlotterComponent({ 6 | material: channelSplitMaterial, 7 | defaultProps: { 8 | id: 'channel-split-component', 9 | text: 'Hello World', 10 | fontFamily: 'sans-serif', 11 | fontSize: 45, 12 | fontWeight: 400, 13 | rotation: 0.0, 14 | rgbOffset: 0.05, 15 | fill: '#4f4f4f', 16 | fontStyle: 'normal', 17 | paddingBottom: 0, 18 | paddingTop: 0, 19 | paddingRight: 0, 20 | paddingLeft: 0, 21 | lineHeight: 1.5 22 | }, 23 | displayName: 'SplitColorChannel', 24 | setMaterialValues: (material, props) => { 25 | material.uniforms.uOffset.value = parseFloat(props.rgbOffset); 26 | material.uniforms.uRotation.value = parseFloat(props.rotation); 27 | material.uniforms.uApplyBlur.value = props.addBlur ? 1.0 : 0.0; 28 | material.uniforms.uAnimateNoise.value = props.addNoise ? 1.0 : 0.0; 29 | } 30 | }); 31 | -------------------------------------------------------------------------------- /src/createBlotterComponent.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { hasBlotterInstance } from './hasBlotterInstance' 3 | 4 | // A high order component that creates a blotter text component using the input parameters 5 | export const createBlotterComponent = ({ 6 | // A material is a function that returns a shader string and uniforms to update the effects 7 | material, 8 | // setMaterialValues is a function that takes a shader material and input props, and updates the materials with those props 9 | // This is invoked on first mount and subsequent state updates 10 | setMaterialValues, 11 | // Default props of the component 12 | defaultProps, 13 | // Component's display name (useful for debugging) 14 | displayName, 15 | }) => { 16 | class BlotterComponent extends React.Component { 17 | material = null 18 | 19 | static displayName = displayName 20 | 21 | static defaultProps = defaultProps 22 | 23 | componentDidMount() { 24 | // Check if the blotter instance is initiated otherwise throw an error 25 | hasBlotterInstance() 26 | 27 | // TODO: Publish a private fork of Blotter with customised build setup 28 | const BlotterInstance = window.Blotter 29 | 30 | // Each material function returns an object which includes a shader string and uniforms to update the effects in shader 31 | const { shader, uniforms } = material(BlotterInstance) 32 | 33 | this.material = new BlotterInstance.ShaderMaterial(shader, { 34 | uniforms, 35 | }) 36 | 37 | // Create a text object with style properties 38 | const text = new BlotterInstance.Text(this.props.text, { 39 | family: this.props.fontFamily, 40 | size: this.props.fontSize, 41 | fill: this.props.fill, 42 | paddingLeft: this.props.paddingLeft, 43 | paddingRight: this.props.paddingRight, 44 | paddingBottom: this.props.paddingBottom, 45 | paddingTop: this.props.paddingTop, 46 | leading: this.props.lineHeight, 47 | weight: this.props.fontWeight, 48 | style: this.props.fontStyle, 49 | }) 50 | 51 | const blotter = new Blotter(this.material, { 52 | texts: text, 53 | }) 54 | 55 | const textObj = blotter.forText(text) 56 | 57 | // Append the text canvas to a user defined element id or wrapper id 58 | this.props.appendTo && typeof this.props.appendTo === 'string' 59 | ? this.appendText(textObj, this.props.appendTo) 60 | : this.appendText(textObj, this.props.id) 61 | 62 | // Invoke the prop callback with rendering context. Useful if you want to update the canvas with other third party libs. 63 | this.props.get2dContext && typeof this.props.get2dContext === 'function' 64 | ? this.props.get2dContext(textObj.context) 65 | : null 66 | 67 | // On first mount, set the material values (this is optional) 68 | setMaterialValues(this.material, this.props) 69 | } 70 | 71 | componentDidUpdate() { 72 | // Update the shader material with new values (or uniforms) 73 | setMaterialValues(this.material, this.props) 74 | } 75 | 76 | appendText = (textObj, id) => { 77 | const element = document.getElementById(id) 78 | 79 | if (element) { 80 | textObj.appendTo(element) 81 | } else { 82 | console.error(`Couldn't find an element with id '${id}'.`) 83 | } 84 | } 85 | 86 | render() { 87 | if (this.props.appendTo) return null 88 | 89 | return
90 | } 91 | } 92 | 93 | return BlotterComponent 94 | } 95 | -------------------------------------------------------------------------------- /src/hasBlotterInstance.js: -------------------------------------------------------------------------------- 1 | export const hasBlotterInstance = () => { 2 | if (typeof window !== undefined && window.Blotter === undefined) { 3 | throw Error(` 4 | Couldn't find a Blotter.js script. Place this script in your HTML file to instantiate Blotter module and WebGL context. 5 | `); 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { SplitColorChannelText } from './SpitColorChannelText'; 2 | export { FliesText } from './FliesText'; 3 | export { LiquidDistortionText } from './LiquidDistortionText'; 4 | export { DistortionText } from './DistortionText'; 5 | -------------------------------------------------------------------------------- /src/materials.js: -------------------------------------------------------------------------------- 1 | // The shaders are copied from https://github.com/bradley/Blotter/tree/master/build/materials. 2 | // Since otherwise we need to have material scripts separately in HTML file, now each component can import 3 | // these materials and update the shader. 4 | 5 | export const channelSplitMaterial = BlotterInstance => { 6 | const shader = [ 7 | BlotterInstance.Assets.Shaders.PI, 8 | BlotterInstance.Assets.Shaders.LineMath, 9 | BlotterInstance.Assets.Shaders.Random, 10 | 11 | 'const int MAX_STEPS = 200;', 12 | 13 | '// Fix a floating point number to two decimal places', 14 | 'float toFixedTwo(float f) {', 15 | ' return float(int(f * 100.0)) / 100.0;', 16 | '}', 17 | 18 | 'vec2 motionBlurOffsets(vec2 fragCoord, float deg, float spread) {', 19 | 20 | ' // Setup', 21 | ' // -------------------------------', 22 | 23 | ' vec2 centerUv = vec2(0.5);', 24 | ' vec2 centerCoord = uResolution.xy * centerUv;', 25 | 26 | ' deg = toFixedTwo(deg);', 27 | ' float slope = normalizedSlope(slopeForDegrees(deg), uResolution.xy);', 28 | 29 | ' // Find offsets', 30 | ' // -------------------------------', 31 | 32 | ' vec2 k = offsetsForCoordAtDistanceOnSlope(spread, slope);', 33 | ' if (deg <= 90.0 || deg >= 270.0) {', 34 | ' k *= -1.0;', 35 | ' }', 36 | 37 | ' return k;', 38 | '}', 39 | 40 | 'float noiseWithWidthAtUv(float width, vec2 uv) {', 41 | ' float noiseModifier = 1.0;', 42 | ' if (uAnimateNoise > 0.0) {', 43 | ' noiseModifier = sin(uGlobalTime);', 44 | ' }', 45 | 46 | ' vec2 noiseRowCol = floor((uv * uResolution.xy) / width);', 47 | ' vec2 noiseFragCoord = ((noiseRowCol * width) + (width / 2.0));', 48 | ' vec2 noiseUv = noiseFragCoord / uResolution.xy;', 49 | 50 | ' return random(noiseUv * noiseModifier) * 0.125;', 51 | '}', 52 | 53 | 'vec4 motionBlur(vec2 uv, vec2 blurOffset, float maxOffset) {', 54 | ' float noiseWidth = 3.0;', 55 | ' float randNoise = noiseWithWidthAtUv(noiseWidth, uv);', 56 | 57 | ' vec4 result = textTexture(uv);', 58 | 59 | ' float maxStepsReached = 0.0;', 60 | 61 | ' // Note: Step by 2 to optimize performance. We conceal lossiness here via applied noise.', 62 | ' // If you do want maximum fidelity, change `i += 2` to `i += 1` below.', 63 | ' for (int i = 1; i <= MAX_STEPS; i += 2) {', 64 | ' if (abs(float(i)) > maxOffset) { break; }', 65 | ' maxStepsReached += 1.0;', 66 | 67 | ' // Divide blurOffset by 2.0 so that motion blur starts half way behind itself', 68 | ' // preventing blur from shoving samples in any direction', 69 | ' vec2 offset = (blurOffset / 2.0) - (blurOffset * (float(i) / maxOffset));', 70 | ' vec4 stepSample = textTexture(uv + (offset / uResolution.xy));', 71 | 72 | , 73 | ' result += stepSample;', 74 | ' }', 75 | 76 | ' if (maxOffset >= 1.0) {', 77 | ' result /= maxStepsReached;', 78 | ' //result.a = pow(result.a, 2.0); // Apply logarithmic smoothing to alpha', 79 | ' result.a -= (randNoise * (1.0 - result.a)); // Apply noise to smoothed alpha', 80 | ' }', 81 | 82 | ' return result;', 83 | '}', 84 | 85 | 'void mainImage( out vec4 mainImage, in vec2 fragCoord ) {', 86 | 87 | ' // Setup', 88 | ' // -------------------------------', 89 | 90 | ' vec2 uv = fragCoord.xy / uResolution.xy;', 91 | 92 | ' float offset = min(float(MAX_STEPS), uResolution.y * uOffset);', 93 | 94 | ' float slope = normalizedSlope(slopeForDegrees(uRotation), uResolution);', 95 | 96 | ' // We want the blur to be the full offset amount in each direction', 97 | ' // and to adjust with our logarithmic adjustment made later, so multiply by 4', 98 | ' float adjustedOffset = offset;// * 4.0;', 99 | 100 | ' vec2 blurOffset = motionBlurOffsets(fragCoord, uRotation, adjustedOffset);', 101 | 102 | ' // Set Starting Points', 103 | ' // -------------------------------', 104 | 105 | ' vec2 rUv = uv;', 106 | ' vec2 gUv = uv;', 107 | ' vec2 bUv = uv;', 108 | 109 | ' vec2 k = offsetsForCoordAtDistanceOnSlope(offset, slope) / uResolution;', 110 | 111 | ' if (uRotation <= 90.0 || uRotation >= 270.0) {', 112 | ' rUv += k;', 113 | ' bUv -= k;', 114 | ' }', 115 | ' else {', 116 | ' rUv -= k;', 117 | ' bUv += k;', 118 | ' }', 119 | 120 | ' // Blur and Split Channels', 121 | ' // -------------------------------', 122 | 123 | ' vec4 resultR = vec4(0.0);', 124 | ' vec4 resultG = vec4(0.0);', 125 | ' vec4 resultB = vec4(0.0);', 126 | 127 | ' if (uApplyBlur > 0.0) {', 128 | ' resultR = motionBlur(rUv, blurOffset, adjustedOffset);', 129 | ' resultG = motionBlur(gUv, blurOffset, adjustedOffset);', 130 | ' resultB = motionBlur(bUv, blurOffset, adjustedOffset);', 131 | ' } else {', 132 | ' resultR = textTexture(rUv);', 133 | ' resultG = textTexture(gUv);', 134 | ' resultB = textTexture(bUv);', 135 | ' }', 136 | 137 | ' float a = resultR.a + resultG.a + resultB.a;', 138 | 139 | ' resultR = normalBlend(resultR, uBlendColor);', 140 | ' resultG = normalBlend(resultG, uBlendColor);', 141 | ' resultB = normalBlend(resultB, uBlendColor);', 142 | 143 | ' mainImage = vec4(resultR.r, resultG.g, resultB.b, a);', 144 | '}' 145 | ].join('\n'); 146 | 147 | const uniforms = { 148 | uOffset: { type: '1f', value: 0.05 }, 149 | uRotation: { type: '1f', value: 45.0 }, 150 | uApplyBlur: { type: '1f', value: 1.0 }, 151 | uAnimateNoise: { type: '1f', value: 1.0 } 152 | }; 153 | 154 | return { 155 | shader, 156 | uniforms 157 | }; 158 | }; 159 | 160 | export const fliesMaterial = BlotterInstance => { 161 | const shader = [ 162 | BlotterInstance.Assets.Shaders.Random, 163 | 164 | 'vec2 random2(vec2 p) {', 165 | ' return fract(sin(vec2(dot(p, vec2(127.1, 311.7)), dot(p, vec2(269.5, 183.3)))) * 43758.5453);', 166 | '}', 167 | 168 | 'float isParticle(out vec3 particleColor, vec2 fragCoord, float pointRadius, float pointCellWidth, float dodge, vec2 dodgePosition, float dodgeSpread, float speed) { ', 169 | ' if (pointCellWidth == 0.0) { return 0.0; };', 170 | 171 | ' vec2 uv = fragCoord.xy / uResolution.xy;', 172 | 173 | ' float pointRadiusOfCell = pointRadius / pointCellWidth;', 174 | 175 | ' vec2 totalCellCount = uResolution.xy / pointCellWidth;', 176 | ' vec2 cellUv = uv * totalCellCount;', 177 | 178 | ' // Tile the space', 179 | ' vec2 iUv = floor(cellUv);', 180 | ' vec2 fUv = fract(cellUv);', 181 | 182 | ' float minDist = 1.0; // minimun distance', 183 | 184 | ' vec4 baseSample = textTexture(cellUv);', 185 | ' float maxWeight = 0.0;', 186 | ' particleColor = baseSample.rgb;', 187 | 188 | ' for (int y= -1; y <= 1; y++) {', 189 | ' for (int x= -1; x <= 1; x++) {', 190 | ' // Neighbor place in the grid', 191 | ' vec2 neighbor = vec2(float(x), float(y));', 192 | 193 | ' // Random position from current + neighbor place in the grid', 194 | ' vec2 point = random2(iUv + neighbor);', 195 | 196 | " // Get cell weighting from cell's center alpha", 197 | ' vec2 cellRowCol = floor(fragCoord / pointCellWidth) + neighbor;', 198 | ' vec2 cellFragCoord = ((cellRowCol * pointCellWidth) + (pointCellWidth / 2.0));', 199 | ' vec4 cellSample = textTexture(cellFragCoord / uResolution.xy);', 200 | ' float cellWeight = cellSample.a;', 201 | 202 | ' if (cellWeight < 1.0) {', 203 | ' // If the cell is not fully within our text, we should disregard it', 204 | ' continue;', 205 | ' }', 206 | 207 | ' maxWeight = max(maxWeight, cellWeight);', 208 | ' if (cellWeight == maxWeight) {', 209 | ' particleColor = cellSample.rgb;', 210 | ' }', 211 | 212 | ' float distanceFromDodge = distance(dodgePosition * uResolution.xy, cellFragCoord) / uResolution.y;', 213 | ' distanceFromDodge = 1.0 - smoothstep(0.0, dodgeSpread, distanceFromDodge);', 214 | 215 | ' // Apply weighting to noise and dodge if dodge is set to 1.0', 216 | ' cellWeight = step(cellWeight, random(cellRowCol)) + (distanceFromDodge * dodge);', 217 | 218 | ' // Animate the point', 219 | ' point = 0.5 + 0.75 * sin((uGlobalTime * speed) + 6.2831 * point);', 220 | 221 | ' // Vector between the pixel and the point', 222 | ' vec2 diff = neighbor + point - fUv;', 223 | 224 | ' // Distance to the point', 225 | ' float dist = length(diff);', 226 | ' dist += cellWeight; // Effectively remove point', 227 | 228 | ' // Keep the closer distance', 229 | ' minDist = min(minDist, dist);', 230 | ' }', 231 | ' }', 232 | 233 | ' float pointEasing = pointRadiusOfCell - (1.0 / pointCellWidth);', 234 | 235 | ' float isParticle = 1.0 - smoothstep(pointEasing, pointRadiusOfCell, minDist);', 236 | 237 | ' return isParticle;', 238 | '}', 239 | 240 | 'void mainImage( out vec4 mainImage, in vec2 fragCoord ) {', 241 | ' vec2 uv = fragCoord.xy / uResolution.xy;', 242 | 243 | ' // Convert uPointCellWidth to pixels, keeping it between 1 and the total y resolution of the text', 244 | ' // Note: floor uPointCellWidth here so that we dont have half pixel widths on retina displays', 245 | ' float pointCellWidth = floor(max(0.0, min(1.0, uPointCellWidth) * uResolution.y));', 246 | 247 | ' // Ensure uPointRadius allow points to exceed the width of their cells', 248 | ' float pointRadius = uPointRadius * 0.8;', 249 | ' pointRadius = min(pointRadius * pointCellWidth, pointCellWidth);', 250 | 251 | ' float dodge = ceil(uDodge);', 252 | 253 | ' vec3 outColor = vec3(0.0);', 254 | ' float point = isParticle(outColor, fragCoord, pointRadius, pointCellWidth, dodge, uDodgePosition, uDodgeSpread, uSpeed);', 255 | 256 | ' mainImage = vec4(outColor, point);', 257 | '}' 258 | ].join('\n'); 259 | 260 | const uniforms = { 261 | uPointCellWidth: { type: '1f', value: 0.04 }, 262 | uPointRadius: { type: '1f', value: 0.75 }, 263 | uDodge: { type: '1f', value: 0.0 }, 264 | uDodgePosition: { type: '2f', value: [0.5, 0.5] }, 265 | uDodgeSpread: { type: '1f', value: 0.25 }, 266 | uSpeed: { type: '1f', value: 1.0 } 267 | }; 268 | 269 | return { 270 | shader, 271 | uniforms 272 | }; 273 | }; 274 | 275 | export const liquidDistortionMaterial = BlotterInstance => { 276 | const shader = [ 277 | BlotterInstance.Assets.Shaders.Noise3D, 278 | 279 | 'void mainImage( out vec4 mainImage, in vec2 fragCoord )', 280 | '{', 281 | ' // Setup ========================================================================', 282 | 283 | ' vec2 uv = fragCoord.xy / uResolution.xy;', 284 | ' float z = uSeed + uGlobalTime * uSpeed;', 285 | 286 | ' uv += snoise(vec3(uv, z)) * uVolatility;', 287 | 288 | ' mainImage = textTexture(uv);', 289 | 290 | '}' 291 | ].join('\n'); 292 | 293 | const uniforms = { 294 | uSpeed: { type: '1f', value: 1.0 }, 295 | uVolatility: { type: '1f', value: 0.15 }, 296 | uSeed: { type: '1f', value: 0.1 } 297 | }; 298 | 299 | return { 300 | shader, 301 | uniforms 302 | }; 303 | }; 304 | 305 | export const distortionText = BlotterInstance => { 306 | const shader = [ 307 | BlotterInstance.Assets.Shaders.PI, 308 | BlotterInstance.Assets.Shaders.LineMath, 309 | BlotterInstance.Assets.Shaders.Noise, 310 | 311 | '// Fix a floating point number to two decimal places', 312 | 'float toFixedTwo(float f) {', 313 | ' return float(int(f * 100.0)) / 100.0;', 314 | '}', 315 | 316 | '// Via: http://www.iquilezles.org/www/articles/functions/functions.htm', 317 | 'float impulse(float k, float x) {', 318 | ' float h = k * x;', 319 | ' return h * exp(1.0 - h);', 320 | '}', 321 | 322 | 'vec2 waveOffset(vec2 fragCoord, float sineDistortSpread, float sineDistortCycleCount, float sineDistortAmplitude, float noiseDistortVolatility, float noiseDistortAmplitude, vec2 distortPosition, float deg, float speed) {', 323 | 324 | ' // Setup', 325 | ' // -------------------------------', 326 | 327 | ' deg = toFixedTwo(deg);', 328 | 329 | ' float centerDistance = 0.5;', 330 | ' vec2 centerUv = vec2(centerDistance);', 331 | ' vec2 centerCoord = uResolution.xy * centerUv;', 332 | 333 | ' float changeOverTime = uGlobalTime * speed;', 334 | 335 | ' float slope = normalizedSlope(slopeForDegrees(deg), uResolution.xy);', 336 | ' float perpendicularDeg = mod(deg + 90.0, 360.0); // Offset angle by 90.0, but keep it from exceeding 360.0', 337 | ' float perpendicularSlope = normalizedSlope(slopeForDegrees(perpendicularDeg), uResolution.xy);', 338 | 339 | ' // Find intersects for line with edges of viewport', 340 | ' // -------------------------------', 341 | 342 | ' vec2 edgeIntersectA = vec2(0.0);', 343 | ' vec2 edgeIntersectB = vec2(0.0);', 344 | ' intersectsOnRectForLine(edgeIntersectA, edgeIntersectB, vec2(0.0), uResolution.xy, centerCoord, slope);', 345 | ' float crossSectionLength = distance(edgeIntersectA, edgeIntersectB);', 346 | 347 | ' // Find the threshold for degrees at which our intersectsOnRectForLine function would flip', 348 | ' // intersects A and B because of the order in which it finds them. This is the angle at which', 349 | ' // the y coordinate for the hypotenuse of a right triangle whose oposite adjacent edge runs from', 350 | ' // vec2(0.0, centerCoord.y) to centerCoord and whose opposite edge runs from vec2(0.0, centerCoord.y) to', 351 | ' // vec2(0.0, uResolution.y) exceeeds uResolution.y', 352 | ' float thresholdDegA = atan(centerCoord.y / centerCoord.x) * (180.0 / PI);', 353 | ' float thresholdDegB = mod(thresholdDegA + 180.0, 360.0);', 354 | 355 | ' vec2 edgeIntersect = vec2(0.0);', 356 | ' if (deg < thresholdDegA || deg > thresholdDegB) {', 357 | ' edgeIntersect = edgeIntersectA;', 358 | ' } else {', 359 | ' edgeIntersect = edgeIntersectB;', 360 | ' }', 361 | 362 | ' vec2 perpendicularIntersectA = vec2(0.0);', 363 | ' vec2 perpendicularIntersectB = vec2(0.0);', 364 | ' intersectsOnRectForLine(perpendicularIntersectA, perpendicularIntersectB, vec2(0.0), uResolution.xy, centerCoord, perpendicularSlope); ', 365 | ' float perpendicularLength = distance(perpendicularIntersectA, perpendicularIntersectA);', 366 | 367 | ' vec2 coordLineIntersect = vec2(0.0);', 368 | ' lineLineIntersection(coordLineIntersect, centerCoord, slope, fragCoord, perpendicularSlope);', 369 | 370 | ' // Define placement for distortion ', 371 | ' // -------------------------------', 372 | 373 | ' vec2 distortPositionIntersect = vec2(0.0);', 374 | ' lineLineIntersection(distortPositionIntersect, distortPosition * uResolution.xy, perpendicularSlope, edgeIntersect, slope);', 375 | ' float distortDistanceFromEdge = (distance(edgeIntersect, distortPositionIntersect) / crossSectionLength);// + sineDistortSpread;', 376 | 377 | ' float uvDistanceFromDistort = distance(edgeIntersect, coordLineIntersect) / crossSectionLength;', 378 | ' float noiseDistortVarianceAdjuster = uvDistanceFromDistort + changeOverTime;', 379 | ' uvDistanceFromDistort += -centerDistance + distortDistanceFromEdge + changeOverTime;', 380 | ' uvDistanceFromDistort = mod(uvDistanceFromDistort, 1.0); // For sine, keep distance between 0.0 and 1.0', 381 | 382 | ' // Define sine distortion ', 383 | ' // -------------------------------', 384 | 385 | ' float minThreshold = centerDistance - sineDistortSpread;', 386 | ' float maxThreshold = centerDistance + sineDistortSpread;', 387 | 388 | ' uvDistanceFromDistort = clamp(((uvDistanceFromDistort - minThreshold)/(maxThreshold - minThreshold)), 0.0, 1.0);', 389 | ' if (sineDistortSpread < 0.5) {', 390 | ' // Add smoother decay to sin distort when it isnt taking up the full viewport.', 391 | ' uvDistanceFromDistort = impulse(uvDistanceFromDistort, uvDistanceFromDistort);', 392 | ' }', 393 | 394 | ' float sineDistortion = sin(uvDistanceFromDistort * PI * sineDistortCycleCount) * sineDistortAmplitude;', 395 | 396 | ' // Define noise distortion ', 397 | ' // -------------------------------', 398 | 399 | ' float noiseDistortion = noise(noiseDistortVolatility * noiseDistortVarianceAdjuster) * noiseDistortAmplitude;', 400 | ' if (noiseDistortVolatility > 0.0) {', 401 | ' noiseDistortion -= noiseDistortAmplitude / 2.0; // Adjust primary distort so that it distorts in two directions.', 402 | ' }', 403 | ' noiseDistortion *= (sineDistortion > 0.0 ? 1.0 : -1.0); // Adjust primary distort to account for sin variance.', 404 | 405 | ' // Combine distortions to find UV offsets ', 406 | ' // -------------------------------', 407 | 408 | ' vec2 kV = offsetsForCoordAtDistanceOnSlope(sineDistortion + noiseDistortion, perpendicularSlope);', 409 | ' if (deg <= 0.0 || deg >= 180.0) {', 410 | ' kV *= -1.0;', 411 | ' }', 412 | 413 | ' return kV;', 414 | '}', 415 | 416 | 'void mainImage( out vec4 mainImage, in vec2 fragCoord )', 417 | '{', 418 | ' // Setup', 419 | ' // -------------------------------', 420 | 421 | ' vec2 uv = fragCoord.xy / uResolution.xy;', 422 | 423 | ' // Minor hacks to ensure our waves start horizontal and animating in a downward direction by default.', 424 | ' uRotation = mod(uRotation + 270.0, 360.0);', 425 | ' uDistortPosition.y = 1.0 - uDistortPosition.y;', 426 | 427 | ' // Distortion', 428 | ' // -------------------------------', 429 | 430 | ' vec2 offset = waveOffset(fragCoord, uSineDistortSpread, uSineDistortCycleCount, uSineDistortAmplitude, uNoiseDistortVolatility, uNoiseDistortAmplitude, uDistortPosition, uRotation, uSpeed);', 431 | 432 | ' mainImage = textTexture(uv + offset);', 433 | '}' 434 | ].join('\n'); 435 | 436 | const uniforms = { 437 | uSineDistortSpread: { type: '1f', value: 0.05 }, 438 | uSineDistortCycleCount: { type: '1f', value: 2.0 }, 439 | uSineDistortAmplitude: { type: '1f', value: 0.25 }, 440 | uNoiseDistortVolatility: { type: '1f', value: 20.0 }, 441 | uNoiseDistortAmplitude: { type: '1f', value: 0.01 }, 442 | uDistortPosition: { type: '2f', value: [0.5, 0.5] }, 443 | uRotation: { type: '1f', value: 170.0 }, 444 | uSpeed: { type: '1f', value: 0.08 } 445 | }; 446 | 447 | return { 448 | shader, 449 | uniforms 450 | }; 451 | }; 452 | -------------------------------------------------------------------------------- /website.js: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled' 2 | import React from 'react' 3 | import ReactDOM from 'react-dom' 4 | import useMobileDetect from 'use-mobile-detect-hook' 5 | 6 | import { SplitColorChannelText, FliesText, LiquidDistortionText, DistortionText } from './src' 7 | 8 | const COLOR = '#2f2f2f' 9 | 10 | const Container = styled.div` 11 | font-family: -apple-system, system-ui, BlinkMacSystemFont; 12 | display: flex; 13 | justify-content: center; 14 | align-items: center; 15 | flex-direction: ${props => props.direction}; 16 | margin-top: ${props => props.top}; 17 | 18 | #description { 19 | color: ${COLOR}; 20 | font-size: 1.4rem; 21 | margin-bottom: 80px; 22 | 23 | a { 24 | text-decoration: none; 25 | border-bottom: 2px solid; 26 | color: inherit; 27 | } 28 | } 29 | 30 | .footer-container { 31 | margin-top: 30px; 32 | } 33 | 34 | footer { 35 | font-size: 1.4rem; 36 | color: inherit; 37 | 38 | a { 39 | text-decoration: none; 40 | border-bottom: 2px solid; 41 | color: inherit; 42 | } 43 | } 44 | 45 | @media only screen and (max-width: 1024px) { 46 | #description { 47 | font-size: 0.9rem; 48 | margin-left: 5px; 49 | margin-top: 20px; 50 | margin-bottom: 30px; 51 | line-height: 1.3rem; 52 | } 53 | 54 | footer { 55 | font-size: 1rem; 56 | } 57 | } 58 | ` 59 | 60 | const LinkButton = styled.a` 61 | text-decoration: none; 62 | padding: 1rem; 63 | border: 4px solid ${COLOR}; 64 | font-size: 1.2rem; 65 | color: ${COLOR}; 66 | font-weigth: bold; 67 | transition: background 0.1s; 68 | border-radius: 5px; 69 | 70 | &:hover { 71 | background-color: ${COLOR}; 72 | color: white; 73 | } 74 | 75 | @media only screen and (max-width: 1024px) { 76 | font-size: 0.9rem; 77 | padding: 0.5rem; 78 | border-width: 2px; 79 | border-radius: 3px; 80 | } 81 | ` 82 | 83 | const Footer = props => ( 84 | 89 | ) 90 | 91 | const App = props => { 92 | const detectMobile = useMobileDetect() 93 | const [value, setValue] = React.useState(0) 94 | const [width, setWidth] = React.useState() 95 | 96 | const ref = React.createRef() 97 | 98 | React.useEffect(() => { 99 | const { width } = ref.current.getBoundingClientRect() 100 | 101 | setWidth(width) 102 | }) 103 | 104 | const updateShader = e => { 105 | if (!detectMobile.isMobile()) { 106 | setValue(e.clientX) 107 | } 108 | } 109 | 110 | return ( 111 | 112 |
113 | 124 |
125 |
126 | A small component library that encapsulates Blotter.js shader 127 | materials in the form of React components and provides a very easy to use API. 128 |
129 |
130 | 131 | Documentation 132 | 133 |
134 |
135 |
136 |
137 |
138 | ) 139 | } 140 | 141 | ReactDOM.render(, document.getElementById('root')) 142 | --------------------------------------------------------------------------------