├── .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 | 
9 |
10 | single layer, reverse movement
11 | 
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 |
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 |
--------------------------------------------------------------------------------