├── .gitignore ├── README.md ├── package.json ├── prettier.config.js ├── public ├── favicon.ico ├── index.html ├── manifest.json ├── out.gif ├── out2.gif └── robots.txt ├── src ├── App.js ├── components │ ├── Control │ │ └── index.jsx │ ├── Effects │ │ ├── index.jsx │ │ └── post │ │ │ ├── Glitchpass.js │ │ │ └── Waterpass.js │ └── Waves │ │ └── index.jsx ├── index.js ├── perlin.js ├── reducer.js ├── serviceWorker.js ├── setupTests.js └── styles.css └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wave 2 | 3 | simple [wave](https://borzecki.github.io/wave/) animation builder, made with react / three.js / a bit of duct tape. 4 | 5 | ## examples 6 | 7 | triple layer perlin noise 8 | ![example with triple layer perlin noise](/public/out.gif) 9 | 10 | single layer, reverse movement 11 | ![reverse single layer](/public/out2.gif) 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wave", 3 | "version": "0.1.0", 4 | "private": true, 5 | "homepage": "https://borzecki.github.io/wave", 6 | "dependencies": { 7 | "@testing-library/jest-dom": "^5.5.0", 8 | "@testing-library/react": "^10.0.2", 9 | "@testing-library/user-event": "^10.0.1", 10 | "lerp": "^1.0.3", 11 | "ramda": "^0.27.0", 12 | "react": "^16.13.1", 13 | "react-dom": "^16.13.1", 14 | "react-scripts": "3.4.1", 15 | "react-three-fiber": "^4.0.28", 16 | "three": "^0.115.0", 17 | "threejs-meshline": "^2.0.10" 18 | }, 19 | "scripts": { 20 | "start": "react-scripts start", 21 | "build": "react-scripts build", 22 | "test": "react-scripts test", 23 | "eject": "react-scripts eject", 24 | "predeploy": "yarn run build", 25 | "deploy": "gh-pages -d build" 26 | }, 27 | "eslintConfig": { 28 | "extends": "react-app" 29 | }, 30 | "browserslist": { 31 | "production": [ 32 | ">0.2%", 33 | "not dead", 34 | "not op_mini all" 35 | ], 36 | "development": [ 37 | "last 1 chrome version", 38 | "last 1 firefox version", 39 | "last 1 safari version" 40 | ] 41 | }, 42 | "devDependencies": { 43 | "gh-pages": "^2.2.0", 44 | "prettier": "^2.0.4" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | tabWidth: 4, 3 | singleQuote: true, 4 | indentSize: 4, 5 | maxLineLength: 120 6 | }; -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borzecki/wave/d4155f1147cbe7c54ce69f59611874eca588fc07/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 14 | 23 | wave 24 | 25 | 26 | 27 |
28 | 38 | 39 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "wave", 3 | "name": "wave - generator", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /public/out.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borzecki/wave/d4155f1147cbe7c54ce69f59611874eca588fc07/public/out.gif -------------------------------------------------------------------------------- /public/out2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borzecki/wave/d4155f1147cbe7c54ce69f59611874eca588fc07/public/out2.gif -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import React, { useState, useReducer } from 'react'; 3 | 4 | import { curry } from 'ramda'; 5 | import { Canvas } from 'react-three-fiber'; 6 | 7 | import Control from './components/Control'; 8 | import Waves from './components/Waves'; 9 | import Effects from './components/Effects'; 10 | 11 | import reducer, { initial } from './reducer'; 12 | 13 | import './styles.css'; 14 | 15 | const App = () => { 16 | const [mouseDown, setMouseDown] = useState(false); 17 | 18 | const [groups, dispatch] = useReducer(reducer, initial); 19 | 20 | const setValue = curry((groupIndex, name, value) => { 21 | dispatch({ type: 'UPDATE_GROUP', groupIndex, name, value }); 22 | }); 23 | 24 | const addControl = () => dispatch({ type: 'ADD_GROUP' }); 25 | const removeControl = i => 26 | dispatch({ type: 'REMOVE_GROUP', groupIndex: i }); 27 | 28 | const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent); 29 | 30 | return ( 31 | <> 32 | setMouseDown(false)} 40 | onMouseDown={() => setMouseDown(true)} 41 | gl={{ 42 | alpha: false, 43 | antialias: true, 44 | logarithmicDepthBuffer: true 45 | }} 46 | onCreated={({ gl }) => { 47 | gl.toneMapping = THREE.ACESFilmicToneMapping; 48 | gl.outputEncoding = THREE.sRGBEncoding; 49 | }} 50 | > 51 | 52 | 53 | 59 | 60 | 61 | 67 | 68 | ); 69 | }; 70 | 71 | export default App; 72 | -------------------------------------------------------------------------------- /src/components/Control/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { map } from 'ramda'; 3 | 4 | const NumberControlGroup = ({ name, value, setValue, step }) => ( 5 |
6 | 7 | setValue(event.target.value)} 13 | /> 14 |
15 | ); 16 | 17 | const ChoiceControlGroup = ({ name, value, options, setValue }) => ( 18 |
19 | 20 | 33 |
34 | ); 35 | 36 | const getControlComponent = { 37 | number: NumberControlGroup, 38 | choice: ChoiceControlGroup 39 | }; 40 | 41 | const ControlGroup = ({ controls, setValue, onRemove }) => ( 42 |
43 |
44 | - 45 |
46 | {map(control => { 47 | const Control = getControlComponent[control.type]; 48 | return ( 49 | 54 | ); 55 | }, controls)} 56 |
57 | ); 58 | 59 | const Control = ({ controlGroups, setValue, onRemove, onAdd }) => ( 60 | <> 61 |
62 | + 63 |
64 |
65 | {controlGroups.map((controls, i) => ( 66 | onRemove(i)} 69 | setValue={setValue(i)} 70 | controls={controls} 71 | /> 72 | ))} 73 |
74 | 75 | ); 76 | 77 | export default Control; 78 | -------------------------------------------------------------------------------- /src/components/Effects/index.jsx: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import React, { useRef, useMemo, useEffect } from 'react'; 3 | import { extend, useThree, useFrame } from 'react-three-fiber'; 4 | import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer'; 5 | import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass'; 6 | import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass'; 7 | import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass'; 8 | import { FilmPass } from 'three/examples/jsm/postprocessing/FilmPass'; 9 | import { GlitchPass } from './post/Glitchpass'; 10 | import { WaterPass } from './post/Waterpass'; 11 | 12 | extend({ 13 | EffectComposer, 14 | ShaderPass, 15 | RenderPass, 16 | WaterPass, 17 | UnrealBloomPass, 18 | FilmPass, 19 | GlitchPass 20 | }); 21 | 22 | export default ({ down }) => { 23 | const composer = useRef(); 24 | const { scene, gl, size, camera } = useThree(); 25 | const aspect = useMemo(() => new THREE.Vector2(size.width, size.height), [ 26 | size 27 | ]); 28 | useEffect(() => void composer.current.setSize(size.width, size.height), [ 29 | size 30 | ]); 31 | useFrame(() => composer.current.render(), 1); 32 | return ( 33 | 34 | 35 | {/* */} 36 | 40 | 41 | 42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /src/components/Effects/post/Glitchpass.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author alteredq / http://alteredqualia.com/ 3 | */ 4 | 5 | /** 6 | * @author felixturner / http://airtight.cc/ 7 | * 8 | * RGB Shift Shader 9 | * Shifts red and blue channels from center in opposite directions 10 | * Ported from http://kriss.cx/tom/2009/05/rgb-shift/ 11 | * by Tom Butterworth / http://kriss.cx/tom/ 12 | * 13 | * amount: shift distance (1 is width of input) 14 | * angle: shift angle in radians 15 | */ 16 | 17 | import { 18 | DataTexture, 19 | FloatType, 20 | Math as _Math, 21 | Mesh, 22 | OrthographicCamera, 23 | PlaneBufferGeometry, 24 | RGBFormat, 25 | Scene, 26 | ShaderMaterial, 27 | UniformsUtils 28 | } from 'three'; 29 | import { Pass } from 'three/examples/jsm/postprocessing/Pass.js'; 30 | 31 | var DigitalGlitch = { 32 | uniforms: { 33 | tDiffuse: { value: null }, //diffuse texture 34 | tDisp: { value: null }, //displacement texture for digital glitch squares 35 | byp: { value: 0 }, //apply the glitch ? 36 | amount: { value: 0.08 }, 37 | angle: { value: 0.02 }, 38 | seed: { value: 0.02 }, 39 | seed_x: { value: 0.02 }, //-1,1 40 | seed_y: { value: 0.02 }, //-1,1 41 | distortion_x: { value: 0.5 }, 42 | distortion_y: { value: 0.6 }, 43 | col_s: { value: 0.05 } 44 | }, 45 | 46 | vertexShader: `varying vec2 vUv; 47 | void main() { 48 | vUv = uv; 49 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); 50 | }`, 51 | 52 | fragmentShader: `uniform int byp; //should we apply the glitch 53 | uniform sampler2D tDiffuse; 54 | uniform sampler2D tDisp; 55 | uniform float amount; 56 | uniform float angle; 57 | uniform float seed; 58 | uniform float seed_x; 59 | uniform float seed_y; 60 | uniform float distortion_x; 61 | uniform float distortion_y; 62 | uniform float col_s; 63 | varying vec2 vUv; 64 | float rand(vec2 co){ 65 | return fract(sin(dot(co.xy ,vec2(12.9898,78.233))) * 43758.5453); 66 | } 67 | void main() { 68 | if(byp<1) { 69 | vec2 p = vUv; 70 | float xs = floor(gl_FragCoord.x / 0.5); 71 | float ys = floor(gl_FragCoord.y / 0.5); 72 | //based on staffantans glitch shader for unity https://github.com/staffantan/unityglitch 73 | vec4 normal = texture2D (tDisp, p*seed*seed); 74 | if(p.ydistortion_x-col_s*seed) { 75 | if(seed_x>0.){ 76 | p.y = 1. - (p.y + distortion_y); 77 | } 78 | else { 79 | p.y = distortion_y; 80 | } 81 | } 82 | p.x+=normal.x*seed_x*(seed/5.); 83 | p.y+=normal.y*seed_y*(seed/5.); 84 | //base from RGB shift shader 85 | vec2 offset = amount * vec2( cos(angle), sin(angle)); 86 | vec4 cr = texture2D(tDiffuse, p + offset); 87 | vec4 cga = texture2D(tDiffuse, p); 88 | vec4 cb = texture2D(tDiffuse, p - offset); 89 | gl_FragColor = vec4(cr.r, cga.g, cb.b, cga.a); 90 | } 91 | else { 92 | gl_FragColor=texture2D (tDiffuse, vUv); 93 | } 94 | }` 95 | }; 96 | 97 | var GlitchPass = function(dt_size) { 98 | Pass.call(this); 99 | if (DigitalGlitch === undefined) 100 | console.error('THREE.GlitchPass relies on THREE.DigitalGlitch'); 101 | var shader = DigitalGlitch; 102 | this.uniforms = UniformsUtils.clone(shader.uniforms); 103 | if (dt_size === undefined) dt_size = 64; 104 | this.uniforms['tDisp'].value = this.generateHeightmap(dt_size); 105 | this.material = new ShaderMaterial({ 106 | uniforms: this.uniforms, 107 | vertexShader: shader.vertexShader, 108 | fragmentShader: shader.fragmentShader 109 | }); 110 | this.camera = new OrthographicCamera(-1, 1, 1, -1, 0, 1); 111 | this.scene = new Scene(); 112 | this.quad = new Mesh(new PlaneBufferGeometry(2, 2), null); 113 | this.quad.frustumCulled = false; // Avoid getting clipped 114 | this.scene.add(this.quad); 115 | this.factor = 0; 116 | }; 117 | 118 | GlitchPass.prototype = Object.assign(Object.create(Pass.prototype), { 119 | constructor: GlitchPass, 120 | 121 | render: function(renderer, writeBuffer, readBuffer, deltaTime, maskActive) { 122 | const factor = Math.max(0, this.factor); 123 | this.uniforms['tDiffuse'].value = readBuffer.texture; 124 | this.uniforms['seed'].value = Math.random() * factor; //default seeding 125 | this.uniforms['byp'].value = 0; 126 | if (factor) { 127 | this.uniforms['amount'].value = (Math.random() / 90) * factor; 128 | this.uniforms['angle'].value = 129 | _Math.randFloat(-Math.PI, Math.PI) * factor; 130 | this.uniforms['distortion_x'].value = 131 | _Math.randFloat(0, 1) * factor; 132 | this.uniforms['distortion_y'].value = 133 | _Math.randFloat(0, 1) * factor; 134 | this.uniforms['seed_x'].value = _Math.randFloat(-0.3, 0.3) * factor; 135 | this.uniforms['seed_y'].value = _Math.randFloat(-0.3, 0.3) * factor; 136 | } else this.uniforms['byp'].value = 1; 137 | this.quad.material = this.material; 138 | if (this.renderToScreen) { 139 | renderer.setRenderTarget(null); 140 | renderer.render(this.scene, this.camera); 141 | } else { 142 | renderer.setRenderTarget(writeBuffer); 143 | if (this.clear) renderer.clear(); 144 | renderer.render(this.scene, this.camera); 145 | } 146 | }, 147 | 148 | generateHeightmap: function(dt_size) { 149 | var data_arr = new Float32Array(dt_size * dt_size * 3); 150 | var length = dt_size * dt_size; 151 | 152 | for (var i = 0; i < length; i++) { 153 | var val = _Math.randFloat(0, 1); 154 | data_arr[i * 3 + 0] = val; 155 | data_arr[i * 3 + 1] = val; 156 | data_arr[i * 3 + 2] = val; 157 | } 158 | 159 | var texture = new DataTexture( 160 | data_arr, 161 | dt_size, 162 | dt_size, 163 | RGBFormat, 164 | FloatType 165 | ); 166 | texture.needsUpdate = true; 167 | return texture; 168 | } 169 | }); 170 | 171 | export { GlitchPass }; 172 | -------------------------------------------------------------------------------- /src/components/Effects/post/Waterpass.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Simple underwater shader 3 | * 4 | 5 | parameters: 6 | tDiffuse: texture 7 | time: this should increase with time passing 8 | distort_speed: how fast you want the distortion effect of water to proceed 9 | distortion: to what degree will the shader distort the screen 10 | centerX: the distortion center X coord 11 | centerY: the distortion center Y coord 12 | 13 | explaination: 14 | the shader is quite simple 15 | it chooses a center and start from there make pixels around it to "swell" then "shrink" then "swell"... 16 | this is of course nothing really similar to underwater scene 17 | but you can combine several this shaders together to create the effect you need... 18 | And yes, this shader could be used for something other than underwater effect, for example, magnifier effect :) 19 | 20 | * @author vergil Wang 21 | */ 22 | 23 | import { 24 | Mesh, 25 | OrthographicCamera, 26 | PlaneBufferGeometry, 27 | Scene, 28 | ShaderMaterial, 29 | UniformsUtils, 30 | Vector2 31 | } from 'three'; 32 | import { Pass } from 'three/examples/jsm/postprocessing/Pass'; 33 | 34 | var WaterShader = { 35 | uniforms: { 36 | byp: { value: 0 }, //apply the glitch ? 37 | texture: { type: 't', value: null }, 38 | time: { type: 'f', value: 0.0 }, 39 | factor: { type: 'f', value: 0.0 }, 40 | resolution: { type: 'v2', value: null } 41 | }, 42 | 43 | vertexShader: `varying vec2 vUv; 44 | void main(){ 45 | vUv = uv; 46 | vec4 modelViewPosition = modelViewMatrix * vec4(position, 1.0); 47 | gl_Position = projectionMatrix * modelViewPosition; 48 | }`, 49 | 50 | fragmentShader: `uniform int byp; //should we apply the glitch ? 51 | uniform float time; 52 | uniform float factor; 53 | uniform vec2 resolution; 54 | uniform sampler2D texture; 55 | 56 | varying vec2 vUv; 57 | 58 | void main() { 59 | if (byp<1) { 60 | vec2 uv1 = vUv; 61 | vec2 uv = gl_FragCoord.xy/resolution.xy; 62 | float frequency = 6.0; 63 | float amplitude = 0.015 * factor; 64 | float x = uv1.y * frequency + time * .7; 65 | float y = uv1.x * frequency + time * .3; 66 | uv1.x += cos(x+y) * amplitude * cos(y); 67 | uv1.y += sin(x-y) * amplitude * cos(y); 68 | vec4 rgba = texture2D(texture, uv1); 69 | gl_FragColor = rgba; 70 | } else { 71 | gl_FragColor = texture2D(texture, vUv); 72 | } 73 | }` 74 | }; 75 | 76 | var WaterPass = function(dt_size) { 77 | Pass.call(this); 78 | if (WaterShader === undefined) 79 | console.error('THREE.WaterPass relies on THREE.WaterShader'); 80 | var shader = WaterShader; 81 | this.uniforms = UniformsUtils.clone(shader.uniforms); 82 | if (dt_size === undefined) dt_size = 64; 83 | this.uniforms['resolution'].value = new Vector2(dt_size, dt_size); 84 | this.material = new ShaderMaterial({ 85 | uniforms: this.uniforms, 86 | vertexShader: shader.vertexShader, 87 | fragmentShader: shader.fragmentShader 88 | }); 89 | this.camera = new OrthographicCamera(-1, 1, 1, -1, 0, 1); 90 | this.scene = new Scene(); 91 | this.quad = new Mesh(new PlaneBufferGeometry(2, 2), null); 92 | this.quad.frustumCulled = false; // Avoid getting clipped 93 | this.scene.add(this.quad); 94 | this.factor = 0; 95 | this.time = 0; 96 | }; 97 | 98 | WaterPass.prototype = Object.assign(Object.create(Pass.prototype), { 99 | constructor: WaterPass, 100 | 101 | render: function(renderer, writeBuffer, readBuffer, deltaTime, maskActive) { 102 | const factor = Math.max(0, this.factor); 103 | this.uniforms['byp'].value = factor ? 0 : 1; 104 | this.uniforms['texture'].value = readBuffer.texture; 105 | this.uniforms['time'].value = this.time; 106 | this.uniforms['factor'].value = this.factor; 107 | this.time += 0.05; 108 | this.quad.material = this.material; 109 | if (this.renderToScreen) { 110 | renderer.setRenderTarget(null); 111 | renderer.render(this.scene, this.camera); 112 | } else { 113 | renderer.setRenderTarget(writeBuffer); 114 | if (this.clear) renderer.clear(); 115 | renderer.render(this.scene, this.camera); 116 | } 117 | } 118 | }); 119 | 120 | export { WaterPass }; 121 | -------------------------------------------------------------------------------- /src/components/Waves/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | import { useFrame } from 'react-three-fiber'; 3 | import { sum, map, fromPairs } from 'ramda'; 4 | import { simplex3, perlin3 } from '../../perlin'; 5 | 6 | const droplets = (x, y, z) => { 7 | const noise = perlin3(x, y, z); 8 | return noise > 0 ? noise : 0; 9 | }; 10 | 11 | const getNoiseFn = { simplex: simplex3, perlin: perlin3, droplets }; 12 | 13 | const Waves = ({ groups, rotateMode, ...props }) => { 14 | const mesh = useRef(); 15 | const groupObjects = map( 16 | group => fromPairs(map(({ name, value }) => [name, value], group)), 17 | groups 18 | ); 19 | useFrame(state => { 20 | const time = state.clock.getElapsedTime(); 21 | 22 | // release the kraken on mouse down 23 | if (rotateMode) { 24 | mesh.current.rotation.x = Math.sin(time / 4); 25 | mesh.current.rotation.y = Math.sin(time / 2); 26 | } else { 27 | mesh.current.rotation.x = 0; 28 | mesh.current.rotation.y = 0; 29 | } 30 | for (var i = 0; i < mesh.current.geometry.vertices.length; i++) { 31 | const { x, y } = mesh.current.geometry.vertices[i]; 32 | const groupValues = map( 33 | ({ coefficient, magnitude, speed, move, method }) => 34 | getNoiseFn[method]( 35 | x * coefficient, 36 | y * coefficient + time * move, 37 | time * speed 38 | ) * magnitude, 39 | groupObjects 40 | ); 41 | mesh.current.geometry.vertices[i].z = sum(groupValues); 42 | } 43 | mesh.current.geometry.verticesNeedUpdate = true; 44 | }); 45 | return ( 46 | 47 | 48 | 49 | 50 | 51 | 52 | ); 53 | }; 54 | 55 | export default Waves; 56 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import * as serviceWorker from './serviceWorker'; 5 | 6 | ReactDOM.render(, document.getElementById('root')); 7 | 8 | // If you want your app to work offline and load faster, you can change 9 | // unregister() to register() below. Note this comes with some pitfalls. 10 | // Learn more about service workers: https://bit.ly/CRA-PWA 11 | serviceWorker.unregister(); 12 | -------------------------------------------------------------------------------- /src/perlin.js: -------------------------------------------------------------------------------- 1 | class Grad { 2 | constructor(x, y, z) { 3 | this.x = x; 4 | this.y = y; 5 | this.z = z; 6 | } 7 | dot2(x, y) { 8 | return this.x * x + this.y * y; 9 | } 10 | dot3(x, y, z) { 11 | return this.x * x + this.y * y + this.z * z; 12 | } 13 | } 14 | 15 | var grad3 = [ 16 | new Grad(1, 1, 0), 17 | new Grad(-1, 1, 0), 18 | new Grad(1, -1, 0), 19 | new Grad(-1, -1, 0), 20 | new Grad(1, 0, 1), 21 | new Grad(-1, 0, 1), 22 | new Grad(1, 0, -1), 23 | new Grad(-1, 0, -1), 24 | new Grad(0, 1, 1), 25 | new Grad(0, -1, 1), 26 | new Grad(0, 1, -1), 27 | new Grad(0, -1, -1) 28 | ]; 29 | 30 | var p = [ 31 | 151, 32 | 160, 33 | 137, 34 | 91, 35 | 90, 36 | 15, 37 | 131, 38 | 13, 39 | 201, 40 | 95, 41 | 96, 42 | 53, 43 | 194, 44 | 233, 45 | 7, 46 | 225, 47 | 140, 48 | 36, 49 | 103, 50 | 30, 51 | 69, 52 | 142, 53 | 8, 54 | 99, 55 | 37, 56 | 240, 57 | 21, 58 | 10, 59 | 23, 60 | 190, 61 | 6, 62 | 148, 63 | 247, 64 | 120, 65 | 234, 66 | 75, 67 | 0, 68 | 26, 69 | 197, 70 | 62, 71 | 94, 72 | 252, 73 | 219, 74 | 203, 75 | 117, 76 | 35, 77 | 11, 78 | 32, 79 | 57, 80 | 177, 81 | 33, 82 | 88, 83 | 237, 84 | 149, 85 | 56, 86 | 87, 87 | 174, 88 | 20, 89 | 125, 90 | 136, 91 | 171, 92 | 168, 93 | 68, 94 | 175, 95 | 74, 96 | 165, 97 | 71, 98 | 134, 99 | 139, 100 | 48, 101 | 27, 102 | 166, 103 | 77, 104 | 146, 105 | 158, 106 | 231, 107 | 83, 108 | 111, 109 | 229, 110 | 122, 111 | 60, 112 | 211, 113 | 133, 114 | 230, 115 | 220, 116 | 105, 117 | 92, 118 | 41, 119 | 55, 120 | 46, 121 | 245, 122 | 40, 123 | 244, 124 | 102, 125 | 143, 126 | 54, 127 | 65, 128 | 25, 129 | 63, 130 | 161, 131 | 1, 132 | 216, 133 | 80, 134 | 73, 135 | 209, 136 | 76, 137 | 132, 138 | 187, 139 | 208, 140 | 89, 141 | 18, 142 | 169, 143 | 200, 144 | 196, 145 | 135, 146 | 130, 147 | 116, 148 | 188, 149 | 159, 150 | 86, 151 | 164, 152 | 100, 153 | 109, 154 | 198, 155 | 173, 156 | 186, 157 | 3, 158 | 64, 159 | 52, 160 | 217, 161 | 226, 162 | 250, 163 | 124, 164 | 123, 165 | 5, 166 | 202, 167 | 38, 168 | 147, 169 | 118, 170 | 126, 171 | 255, 172 | 82, 173 | 85, 174 | 212, 175 | 207, 176 | 206, 177 | 59, 178 | 227, 179 | 47, 180 | 16, 181 | 58, 182 | 17, 183 | 182, 184 | 189, 185 | 28, 186 | 42, 187 | 223, 188 | 183, 189 | 170, 190 | 213, 191 | 119, 192 | 248, 193 | 152, 194 | 2, 195 | 44, 196 | 154, 197 | 163, 198 | 70, 199 | 221, 200 | 153, 201 | 101, 202 | 155, 203 | 167, 204 | 43, 205 | 172, 206 | 9, 207 | 129, 208 | 22, 209 | 39, 210 | 253, 211 | 19, 212 | 98, 213 | 108, 214 | 110, 215 | 79, 216 | 113, 217 | 224, 218 | 232, 219 | 178, 220 | 185, 221 | 112, 222 | 104, 223 | 218, 224 | 246, 225 | 97, 226 | 228, 227 | 251, 228 | 34, 229 | 242, 230 | 193, 231 | 238, 232 | 210, 233 | 144, 234 | 12, 235 | 191, 236 | 179, 237 | 162, 238 | 241, 239 | 81, 240 | 51, 241 | 145, 242 | 235, 243 | 249, 244 | 14, 245 | 239, 246 | 107, 247 | 49, 248 | 192, 249 | 214, 250 | 31, 251 | 181, 252 | 199, 253 | 106, 254 | 157, 255 | 184, 256 | 84, 257 | 204, 258 | 176, 259 | 115, 260 | 121, 261 | 50, 262 | 45, 263 | 127, 264 | 4, 265 | 150, 266 | 254, 267 | 138, 268 | 236, 269 | 205, 270 | 93, 271 | 222, 272 | 114, 273 | 67, 274 | 29, 275 | 24, 276 | 72, 277 | 243, 278 | 141, 279 | 128, 280 | 195, 281 | 78, 282 | 66, 283 | 215, 284 | 61, 285 | 156, 286 | 180 287 | ]; 288 | // To remove the need for index wrapping, double the permutation table length 289 | var perm = new Array(512); 290 | var gradP = new Array(512); 291 | 292 | // This isn't a very good seeding function, but it works ok. It supports 2^16 293 | // different seed values. Write something better if you need more seeds. 294 | export const seed = function(seed) { 295 | if (seed > 0 && seed < 1) { 296 | // Scale the seed out 297 | seed *= 65536; 298 | } 299 | 300 | seed = Math.floor(seed); 301 | if (seed < 256) { 302 | seed |= seed << 8; 303 | } 304 | 305 | for (var i = 0; i < 256; i++) { 306 | var v; 307 | if (i & 1) { 308 | v = p[i] ^ (seed & 255); 309 | } else { 310 | v = p[i] ^ ((seed >> 8) & 255); 311 | } 312 | 313 | perm[i] = perm[i + 256] = v; 314 | gradP[i] = gradP[i + 256] = grad3[v % 12]; 315 | } 316 | }; 317 | 318 | seed(0); 319 | 320 | /* 321 | for(var i=0; i<256; i++) { 322 | perm[i] = perm[i + 256] = p[i]; 323 | gradP[i] = gradP[i + 256] = grad3[perm[i] % 12]; 324 | }*/ 325 | 326 | // Skewing and unskewing factors for 2, 3, and 4 dimensions 327 | var F2 = 0.5 * (Math.sqrt(3) - 1); 328 | var G2 = (3 - Math.sqrt(3)) / 6; 329 | 330 | var F3 = 1 / 3; 331 | var G3 = 1 / 6; 332 | 333 | // 2D simplex noise 334 | export const simplex2 = function(xin, yin) { 335 | var n0, n1, n2; // Noise contributions from the three corners 336 | // Skew the input space to determine which simplex cell we're in 337 | var s = (xin + yin) * F2; // Hairy factor for 2D 338 | var i = Math.floor(xin + s); 339 | var j = Math.floor(yin + s); 340 | var t = (i + j) * G2; 341 | var x0 = xin - i + t; // The x,y distances from the cell origin, unskewed. 342 | var y0 = yin - j + t; 343 | // For the 2D case, the simplex shape is an equilateral triangle. 344 | // Determine which simplex we are in. 345 | var i1, j1; // Offsets for second (middle) corner of simplex in (i,j) coords 346 | if (x0 > y0) { 347 | // lower triangle, XY order: (0,0)->(1,0)->(1,1) 348 | i1 = 1; 349 | j1 = 0; 350 | } else { 351 | // upper triangle, YX order: (0,0)->(0,1)->(1,1) 352 | i1 = 0; 353 | j1 = 1; 354 | } 355 | // A step of (1,0) in (i,j) means a step of (1-c,-c) in (x,y), and 356 | // a step of (0,1) in (i,j) means a step of (-c,1-c) in (x,y), where 357 | // c = (3-sqrt(3))/6 358 | var x1 = x0 - i1 + G2; // Offsets for middle corner in (x,y) unskewed coords 359 | var y1 = y0 - j1 + G2; 360 | var x2 = x0 - 1 + 2 * G2; // Offsets for last corner in (x,y) unskewed coords 361 | var y2 = y0 - 1 + 2 * G2; 362 | // Work out the hashed gradient indices of the three simplex corners 363 | i &= 255; 364 | j &= 255; 365 | var gi0 = gradP[i + perm[j]]; 366 | var gi1 = gradP[i + i1 + perm[j + j1]]; 367 | var gi2 = gradP[i + 1 + perm[j + 1]]; 368 | // Calculate the contribution from the three corners 369 | var t0 = 0.5 - x0 * x0 - y0 * y0; 370 | if (t0 < 0) { 371 | n0 = 0; 372 | } else { 373 | t0 *= t0; 374 | n0 = t0 * t0 * gi0.dot2(x0, y0); // (x,y) of grad3 used for 2D gradient 375 | } 376 | var t1 = 0.5 - x1 * x1 - y1 * y1; 377 | if (t1 < 0) { 378 | n1 = 0; 379 | } else { 380 | t1 *= t1; 381 | n1 = t1 * t1 * gi1.dot2(x1, y1); 382 | } 383 | var t2 = 0.5 - x2 * x2 - y2 * y2; 384 | if (t2 < 0) { 385 | n2 = 0; 386 | } else { 387 | t2 *= t2; 388 | n2 = t2 * t2 * gi2.dot2(x2, y2); 389 | } 390 | // Add contributions from each corner to get the final noise value. 391 | // The result is scaled to return values in the interval [-1,1]. 392 | return 70 * (n0 + n1 + n2); 393 | }; 394 | 395 | // 3D simplex noise 396 | export const simplex3 = function(xin, yin, zin) { 397 | var n0, n1, n2, n3; // Noise contributions from the four corners 398 | 399 | // Skew the input space to determine which simplex cell we're in 400 | var s = (xin + yin + zin) * F3; // Hairy factor for 2D 401 | var i = Math.floor(xin + s); 402 | var j = Math.floor(yin + s); 403 | var k = Math.floor(zin + s); 404 | 405 | var t = (i + j + k) * G3; 406 | var x0 = xin - i + t; // The x,y distances from the cell origin, unskewed. 407 | var y0 = yin - j + t; 408 | var z0 = zin - k + t; 409 | 410 | // For the 3D case, the simplex shape is a slightly irregular tetrahedron. 411 | // Determine which simplex we are in. 412 | var i1, j1, k1; // Offsets for second corner of simplex in (i,j,k) coords 413 | var i2, j2, k2; // Offsets for third corner of simplex in (i,j,k) coords 414 | if (x0 >= y0) { 415 | if (y0 >= z0) { 416 | i1 = 1; 417 | j1 = 0; 418 | k1 = 0; 419 | i2 = 1; 420 | j2 = 1; 421 | k2 = 0; 422 | } else if (x0 >= z0) { 423 | i1 = 1; 424 | j1 = 0; 425 | k1 = 0; 426 | i2 = 1; 427 | j2 = 0; 428 | k2 = 1; 429 | } else { 430 | i1 = 0; 431 | j1 = 0; 432 | k1 = 1; 433 | i2 = 1; 434 | j2 = 0; 435 | k2 = 1; 436 | } 437 | } else { 438 | if (y0 < z0) { 439 | i1 = 0; 440 | j1 = 0; 441 | k1 = 1; 442 | i2 = 0; 443 | j2 = 1; 444 | k2 = 1; 445 | } else if (x0 < z0) { 446 | i1 = 0; 447 | j1 = 1; 448 | k1 = 0; 449 | i2 = 0; 450 | j2 = 1; 451 | k2 = 1; 452 | } else { 453 | i1 = 0; 454 | j1 = 1; 455 | k1 = 0; 456 | i2 = 1; 457 | j2 = 1; 458 | k2 = 0; 459 | } 460 | } 461 | // A step of (1,0,0) in (i,j,k) means a step of (1-c,-c,-c) in (x,y,z), 462 | // a step of (0,1,0) in (i,j,k) means a step of (-c,1-c,-c) in (x,y,z), and 463 | // a step of (0,0,1) in (i,j,k) means a step of (-c,-c,1-c) in (x,y,z), where 464 | // c = 1/6. 465 | var x1 = x0 - i1 + G3; // Offsets for second corner 466 | var y1 = y0 - j1 + G3; 467 | var z1 = z0 - k1 + G3; 468 | 469 | var x2 = x0 - i2 + 2 * G3; // Offsets for third corner 470 | var y2 = y0 - j2 + 2 * G3; 471 | var z2 = z0 - k2 + 2 * G3; 472 | 473 | var x3 = x0 - 1 + 3 * G3; // Offsets for fourth corner 474 | var y3 = y0 - 1 + 3 * G3; 475 | var z3 = z0 - 1 + 3 * G3; 476 | 477 | // Work out the hashed gradient indices of the four simplex corners 478 | i &= 255; 479 | j &= 255; 480 | k &= 255; 481 | var gi0 = gradP[i + perm[j + perm[k]]]; 482 | var gi1 = gradP[i + i1 + perm[j + j1 + perm[k + k1]]]; 483 | var gi2 = gradP[i + i2 + perm[j + j2 + perm[k + k2]]]; 484 | var gi3 = gradP[i + 1 + perm[j + 1 + perm[k + 1]]]; 485 | 486 | // Calculate the contribution from the four corners 487 | var t0 = 0.6 - x0 * x0 - y0 * y0 - z0 * z0; 488 | if (t0 < 0) { 489 | n0 = 0; 490 | } else { 491 | t0 *= t0; 492 | n0 = t0 * t0 * gi0.dot3(x0, y0, z0); // (x,y) of grad3 used for 2D gradient 493 | } 494 | var t1 = 0.6 - x1 * x1 - y1 * y1 - z1 * z1; 495 | if (t1 < 0) { 496 | n1 = 0; 497 | } else { 498 | t1 *= t1; 499 | n1 = t1 * t1 * gi1.dot3(x1, y1, z1); 500 | } 501 | var t2 = 0.6 - x2 * x2 - y2 * y2 - z2 * z2; 502 | if (t2 < 0) { 503 | n2 = 0; 504 | } else { 505 | t2 *= t2; 506 | n2 = t2 * t2 * gi2.dot3(x2, y2, z2); 507 | } 508 | var t3 = 0.6 - x3 * x3 - y3 * y3 - z3 * z3; 509 | if (t3 < 0) { 510 | n3 = 0; 511 | } else { 512 | t3 *= t3; 513 | n3 = t3 * t3 * gi3.dot3(x3, y3, z3); 514 | } 515 | // Add contributions from each corner to get the final noise value. 516 | // The result is scaled to return values in the interval [-1,1]. 517 | return 32 * (n0 + n1 + n2 + n3); 518 | }; 519 | 520 | // ##### Perlin noise stuff 521 | 522 | function fade(t) { 523 | return t * t * t * (t * (t * 6 - 15) + 10); 524 | } 525 | 526 | function lerp(a, b, t) { 527 | return (1 - t) * a + t * b; 528 | } 529 | 530 | // 2D Perlin Noise 531 | export const perlin2 = function(x, y) { 532 | // Find unit grid cell containing point 533 | var X = Math.floor(x), 534 | Y = Math.floor(y); 535 | // Get relative xy coordinates of point within that cell 536 | x = x - X; 537 | y = y - Y; 538 | // Wrap the integer cells at 255 (smaller integer period can be introduced here) 539 | X = X & 255; 540 | Y = Y & 255; 541 | 542 | // Calculate noise contributions from each of the four corners 543 | var n00 = gradP[X + perm[Y]].dot2(x, y); 544 | var n01 = gradP[X + perm[Y + 1]].dot2(x, y - 1); 545 | var n10 = gradP[X + 1 + perm[Y]].dot2(x - 1, y); 546 | var n11 = gradP[X + 1 + perm[Y + 1]].dot2(x - 1, y - 1); 547 | 548 | // Compute the fade curve value for x 549 | var u = fade(x); 550 | 551 | // Interpolate the four results 552 | return lerp(lerp(n00, n10, u), lerp(n01, n11, u), fade(y)); 553 | }; 554 | 555 | // 3D Perlin Noise 556 | export const perlin3 = function(x, y, z) { 557 | // Find unit grid cell containing point 558 | var X = Math.floor(x), 559 | Y = Math.floor(y), 560 | Z = Math.floor(z); 561 | // Get relative xyz coordinates of point within that cell 562 | x = x - X; 563 | y = y - Y; 564 | z = z - Z; 565 | // Wrap the integer cells at 255 (smaller integer period can be introduced here) 566 | X = X & 255; 567 | Y = Y & 255; 568 | Z = Z & 255; 569 | 570 | // Calculate noise contributions from each of the eight corners 571 | var n000 = gradP[X + perm[Y + perm[Z]]].dot3(x, y, z); 572 | var n001 = gradP[X + perm[Y + perm[Z + 1]]].dot3(x, y, z - 1); 573 | var n010 = gradP[X + perm[Y + 1 + perm[Z]]].dot3(x, y - 1, z); 574 | var n011 = gradP[X + perm[Y + 1 + perm[Z + 1]]].dot3(x, y - 1, z - 1); 575 | var n100 = gradP[X + 1 + perm[Y + perm[Z]]].dot3(x - 1, y, z); 576 | var n101 = gradP[X + 1 + perm[Y + perm[Z + 1]]].dot3(x - 1, y, z - 1); 577 | var n110 = gradP[X + 1 + perm[Y + 1 + perm[Z]]].dot3(x - 1, y - 1, z); 578 | var n111 = gradP[X + 1 + perm[Y + 1 + perm[Z + 1]]].dot3( 579 | x - 1, 580 | y - 1, 581 | z - 1 582 | ); 583 | 584 | // Compute the fade curve value for x, y, z 585 | var u = fade(x); 586 | var v = fade(y); 587 | var w = fade(z); 588 | 589 | // Interpolate 590 | return lerp( 591 | lerp(lerp(n000, n100, u), lerp(n001, n101, u), w), 592 | lerp(lerp(n010, n110, u), lerp(n011, n111, u), w), 593 | v 594 | ); 595 | }; 596 | -------------------------------------------------------------------------------- /src/reducer.js: -------------------------------------------------------------------------------- 1 | import { remove, append, last, findIndex, propEq, adjust, assoc } from 'ramda'; 2 | 3 | const defaultGroup = [ 4 | { name: 'coefficient', value: 0.01, step: 0.01, type: 'number' }, 5 | { name: 'magnitude', value: 25, step: 1, type: 'number' }, 6 | { name: 'speed', value: 1, step: 1, type: 'number' }, 7 | { name: 'move', value: -1, step: 1, type: 'number' }, 8 | { 9 | name: 'method', 10 | value: 'simplex', 11 | type: 'choice', 12 | options: ['simplex', 'perlin', 'droplets'] 13 | } 14 | ]; 15 | export const initial = [defaultGroup]; 16 | 17 | export default (state, { type, groupIndex, name, value }) => { 18 | switch (type) { 19 | case 'ADD_GROUP': 20 | return append(state.length ? last(state) : defaultGroup, state); 21 | case 'REMOVE_GROUP': 22 | return remove(groupIndex, 1, state); 23 | case 'UPDATE_GROUP': 24 | const group = state[groupIndex]; 25 | const controlIndex = findIndex(propEq('name', name), group); 26 | const updateControl = () => 27 | adjust( 28 | controlIndex, 29 | () => assoc('value', value, group[controlIndex]), 30 | group 31 | ); 32 | return adjust(groupIndex, updateControl, state); 33 | default: 34 | return initial; 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' } 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready 134 | .then(registration => { 135 | registration.unregister(); 136 | }) 137 | .catch(error => { 138 | console.error(error.message); 139 | }); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | #root { 4 | width: 100%; 5 | height: 100%; 6 | margin: 0; 7 | padding: 0; 8 | -webkit-touch-callout: none; 9 | -webkit-user-select: none; 10 | -khtml-user-select: none; 11 | -moz-user-select: none; 12 | -ms-user-select: none; 13 | user-select: none; 14 | overflow: hidden; 15 | } 16 | 17 | #root { 18 | overflow: auto; 19 | } 20 | 21 | *, 22 | *::after, 23 | *::before { 24 | box-sizing: border-box; 25 | } 26 | 27 | :root { 28 | font-size: 20px; 29 | } 30 | 31 | ::selection { 32 | background: #2ddab8; 33 | color: white; 34 | } 35 | 36 | .controls { 37 | position: absolute; 38 | top: 20px; 39 | right: 30px; 40 | } 41 | 42 | .control { 43 | display: block; 44 | padding: 10px; 45 | position: relative; 46 | } 47 | 48 | .control-group { 49 | padding: 5px; 50 | } 51 | 52 | input { 53 | border: 0; 54 | width: 70px; 55 | text-align: right; 56 | outline: 0; 57 | font-size: 0.8rem; 58 | color: rgba(255, 255, 255, 0.2); 59 | padding: 2px 0; 60 | float: right; 61 | background: transparent; 62 | transition: border-color 0.2s; 63 | } 64 | 65 | input:focus { 66 | padding-bottom: 2px; 67 | font-weight: 700; 68 | color: rgba(255, 255, 255, 0.7); 69 | border-width: 3px; 70 | border-color: #fff; 71 | } 72 | 73 | label { 74 | color: rgba(255, 255, 255, 0.2); 75 | font-size: 0.8rem; 76 | font-weight: 700; 77 | padding-right: 20px; 78 | } 79 | 80 | /* Chrome, Safari, Edge, Opera */ 81 | input::-webkit-outer-spin-button, 82 | input::-webkit-inner-spin-button { 83 | -webkit-appearance: none; 84 | margin: 0; 85 | } 86 | 87 | /* Firefox */ 88 | input[type='number'] { 89 | -moz-appearance: textfield; 90 | } 91 | 92 | .add-control { 93 | position: absolute; 94 | top: 10px; 95 | right: 10px; 96 | font-size: 20px; 97 | text-align: center; 98 | height: 20px; 99 | width: 20px; 100 | color: #fff; 101 | border-radius: 100%; 102 | transition: 1s cubic-bezier(0.075, 0.82, 0.165, 1); 103 | } 104 | 105 | .add-control:hover { 106 | background-color: #d44949; 107 | } 108 | 109 | .remove-control { 110 | position: absolute; 111 | top: 16px; 112 | right: -20px; 113 | font-size: 20px; 114 | text-align: center; 115 | height: 20px; 116 | width: 20px; 117 | color: #fff; 118 | border-radius: 100%; 119 | transition: 1s cubic-bezier(0.075, 0.82, 0.165, 1); 120 | } 121 | 122 | .remove-control:hover { 123 | background-color: #d44949; 124 | } 125 | 126 | body { 127 | margin: 0; 128 | padding: 0; 129 | background: #0c0f13; 130 | overflow: hidden; 131 | font-family: 'Sulphur Point', sans-serif; 132 | color: white; 133 | font-size: 0.9rem; 134 | cursor: pointer; 135 | } 136 | 137 | select { 138 | font-size: 16px; 139 | font-family: sans-serif; 140 | padding: 2px 5px; 141 | text-align-last: center; 142 | background: transparent; 143 | border-color: transparent; 144 | float: right; 145 | color: rgba(255, 255, 255, 0.2); 146 | border-radius: 0.2em; 147 | -moz-appearance: none; 148 | -webkit-appearance: none; 149 | appearance: none; 150 | } 151 | select::-ms-expand { 152 | display: none; 153 | } 154 | select:hover { 155 | border-color: #888; 156 | } 157 | select:focus { 158 | outline: none; 159 | } 160 | select option { 161 | font-weight: normal; 162 | } 163 | --------------------------------------------------------------------------------