Эффект шестигранной мозайки
Эффект мозайки на изображении реализованной с помощью three.js и шейдеров.
HTML
<div id="js-app" class="c-container"></div>
CSS
body {
margin: 0;
padding: 0;
background-color: #000;
}
canvas {
display: block;
margin: 0 auto;
}
.c-container {
height: 100vh;
}
JS
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.7.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.7.0/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prop-types/15.7.2/prop-types.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/101/three.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.6/dat.gui.min.js"></script>
<script>(function(){var script=document.createElement('script');script.onload=function(){var stats=new Stats();document.body.appendChild(stats.dom);requestAnimationFrame(function loop(){stats.update();requestAnimationFrame(loop)});};script.src='//rawgit.com/mrdoob/stats.js/master/build/stats.min.js';document.head.appendChild(script);})()</script>
<script id="hsv2rgb" type="shader/fragment">
vec3 hsv2rgb (vec3 c) {
vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}
</script>
<script id="noise2d" type="shader/fragment">
//
// Description : Array and textureless GLSL 2D/3D/4D simplex
// noise functions.
// Author : Ian McEwan, Ashima Arts.
// Maintainer : stegu
// Lastmod : 20110822 (ijm)
// License : Copyright (C) 2011 Ashima Arts. All rights reserved.
// Distributed under the MIT License. See LICENSE file.
// https://github.com/ashima/webgl-noise
// https://github.com/stegu/webgl-noise
//
vec3 mod289(vec3 x) {
return x - floor(x * (1.0 / 289.0)) * 289.0;
}
vec4 mod289(vec4 x) {
return x - floor(x * (1.0 / 289.0)) * 289.0;
}
vec4 permute(vec4 x) {
return mod289(((x*34.0)+1.0)*x);
}
vec4 taylorInvSqrt(vec4 r)
{
return 1.79284291400159 - 0.85373472095314 * r;
}
float snoise(vec3 v)
{
const vec2 C = vec2(1.0/6.0, 1.0/3.0) ;
const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
// First corner
vec3 i = floor(v + dot(v, C.yyy) );
vec3 x0 = v - i + dot(i, C.xxx) ;
// Other corners
vec3 g = step(x0.yzx, x0.xyz);
vec3 l = 1.0 - g;
vec3 i1 = min( g.xyz, l.zxy );
vec3 i2 = max( g.xyz, l.zxy );
// x0 = x0 - 0.0 + 0.0 * C.xxx;
// x1 = x0 - i1 + 1.0 * C.xxx;
// x2 = x0 - i2 + 2.0 * C.xxx;
// x3 = x0 - 1.0 + 3.0 * C.xxx;
vec3 x1 = x0 - i1 + C.xxx;
vec3 x2 = x0 - i2 + C.yyy; // 2.0*C.x = 1/3 = C.y
vec3 x3 = x0 - D.yyy; // -1.0+3.0*C.x = -0.5 = -D.y
// Permutations
i = mod289(i);
vec4 p = permute( permute( permute(
i.z + vec4(0.0, i1.z, i2.z, 1.0 ))
+ i.y + vec4(0.0, i1.y, i2.y, 1.0 ))
+ i.x + vec4(0.0, i1.x, i2.x, 1.0 ));
// Gradients: 7x7 points over a square, mapped onto an octahedron.
// The ring size 17*17 = 289 is close to a multiple of 49 (49*6 = 294)
float n_ = 0.142857142857; // 1.0/7.0
vec3 ns = n_ * D.wyz - D.xzx;
vec4 j = p - 49.0 * floor(p * ns.z * ns.z); // mod(p,7*7)
vec4 x_ = floor(j * ns.z);
vec4 y_ = floor(j - 7.0 * x_ ); // mod(j,N)
vec4 x = x_ *ns.x + ns.yyyy;
vec4 y = y_ *ns.x + ns.yyyy;
vec4 h = 1.0 - abs(x) - abs(y);
vec4 b0 = vec4( x.xy, y.xy );
vec4 b1 = vec4( x.zw, y.zw );
//vec4 s0 = vec4(lessThan(b0,0.0))*2.0 - 1.0;
//vec4 s1 = vec4(lessThan(b1,0.0))*2.0 - 1.0;
vec4 s0 = floor(b0)*2.0 + 1.0;
vec4 s1 = floor(b1)*2.0 + 1.0;
vec4 sh = -step(h, vec4(0.0));
vec4 a0 = b0.xzyw + s0.xzyw*sh.xxyy ;
vec4 a1 = b1.xzyw + s1.xzyw*sh.zzww ;
vec3 p0 = vec3(a0.xy,h.x);
vec3 p1 = vec3(a0.zw,h.y);
vec3 p2 = vec3(a1.xy,h.z);
vec3 p3 = vec3(a1.zw,h.w);
//Normalise gradients
vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3)));
p0 *= norm.x;
p1 *= norm.y;
p2 *= norm.z;
p3 *= norm.w;
// Mix final noise value
vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);
m = m * m;
return 42.0 * dot( m*m, vec4( dot(p0,x0), dot(p1,x1),
dot(p2,x2), dot(p3,x3) ) );
}
</script>
const images = {
'In the back': 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/292951/photo-41.JPG',
'Quatro doggo': 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/292951/photo-26.JPG',
'Puppy puddle': 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/292951/hot%20rod%20vic%20001.jpg',
'In a field': 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/292951/photo-1.JPG',
'Cat caution': 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/292951/img_2.jpg'
}
setTimeout(() => {
const props = {
imageSrc: images['In the back'],
tileSize: 20,
scopeSize: 0.4,
showScope: true,
filterEnabled: true
}
const render = () => {
ReactDOM.render((
<Shader
{...props}
{...{
vertexShader,
fragmentShader
}}
/>
), document.getElementById('js-app'))
}
setupGui(props, render)
render()
}, 0)
const setupGui = (props, render) => {
const gui = new dat.GUI()
const imageSrcField = gui.add(props, 'imageSrc', images)
const tileSizeField = gui.add(props, 'tileSize', 1, 40).step(1)
const scopeSizeField = gui.add(props, 'scopeSize', 0.1, 2.0).step(0.05)
const showScopeField = gui.add(props, 'showScope')
const filterEnabledField = gui.add(props, 'filterEnabled')
imageSrcField.onchange(() => render())
tileSizeField.onchange(() => render())
scopeSizeField.onchange(() => render())
showScopeField.onchange(() => render())
filterEnabledField.onchange(() => render())
}
const vertexShader = `
void main () {
gl_Position = vec4(position, 1.0);
}
`
const fragmentShader = `
${document.getElementById('noise2d').text}
${document.getElementById('hsv2rgb').text}
uniform vec2 uResolution;
uniform float uTime;
uniform vec2 uMouse;
uniform sampler2D uImage;
uniform float uGridWidth;
uniform float uMagRadius;
uniform bool uShowScope;
uniform bool uFilterEnabled;
const float EPSILON = 0.01;
vec3 cubeRound (vec3 cube) {
float rx = floor(cube.x + 0.5);
float ry = floor(cube.y + 0.5);
float rz = floor(cube.z + 0.5);
float xd = abs(rx - cube.x);
float yd = abs(ry - cube.y);
float zd = abs(rz - cube.z);
if (xd > yd && xd > zd) {
rx = -ry - rz;
} else if (yd > zd) {
ry = -rx - rz;
} else {
rz = -rx - ry;
}
return vec3(rx, ry, rz);
}
vec2 cubeToAxial (vec3 cube) {
return cube.xz;
}
vec3 axialToCube (vec2 hex) {
float x = hex.x;
float z = hex.y;
float y = -x - z;
return vec3(x, y, z);
}
vec2 hexRound (vec2 hex) {
return cubeToAxial(cubeRound(axialToCube(hex)));
}
vec2 pointToAxial (vec2 point) {
float q = (sqrt(3.0) / 3.0 * point.x - 1.0 / 3.0 * point.y);
float r = ( 2.0 / 3.0 * point.y);
return vec2(q, r);
}
vec2 axialToPoint (vec2 axial) {
float y = axial.y / (2.0 / 3.0);
float x = (((1.0 / 3.0) * y) + axial.x) / (sqrt(3.0) / 3.0);
return (vec2(x, y)) / 2.0 - 0.5;
}
vec2 pointToHex (vec2 point) {
vec2 qr = pointToAxial(point);
return hexRound(vec3(qr.x, qr.y, 5625463739.0).xy);
}
float circle (vec2 pos, float r) {
return 1.0 - smoothstep(r - (r * EPSILON), r + (r * EPSILON), dot(pos, pos) * 4.0);
}
vec2 convertPoint (vec2 point) {
vec2 pt = point.xy / uResolution.xy;
pt = (pt * 2.0) - 1.0;
return pt;
}
vec2 getMagPos () {
return convertPoint(uMouse);
}
float getMag (vec2 pt, vec2 pos, float radius) {
return circle(((pt - pos)), radius);
}
void main () {
vec2 pt = convertPoint(gl_FragCoord.xy);
vec2 aspect = vec2(uResolution.x / uResolution.y, 1.0);
vec2 aspectPt = aspect * pt;
vec2 magPos = getMagPos() * aspect;
float magnification = getMag(aspectPt, magPos, uMagRadius);
vec2 sample = pointToAxial(pt);
if ((!uShowScope || magnification == 0.0) && uFilterEnabled) {
sample = pointToHex(pt * uGridWidth) / uGridWidth;
}
vec3 color = texture2D(uImage, axialToPoint(sample)).rgb;
float outline = (getMag(aspectPt, magPos, uMagRadius) - getMag(aspectPt, magPos, uMagRadius * 0.9)) / 4.0;
if (!uShowScope) {
outline = 0.0;
}
gl_FragColor = vec4(color, 1.0) + outline;
}
`
class Shader extends React.Component {
static propTypes = {
imageSrc: PropTypes.string.isRequired,
tileSize: PropTypes.number.isRequired,
fragmentShader: PropTypes.string.isRequired,
vertexShader: PropTypes.string.isRequired,
scopeSize: PropTypes.number.isRequired,
showScope: PropTypes.bool.isRequired,
filterEnabled: PropTypes.bool.isRequired
}
shouldComponentUpdate () {
return false
}
async componentDidMount () {
const camera = new THREE.Camera()
camera.position.z = 1
const scene = new THREE.Scene()
const geometry = new THREE.PlaneBufferGeometry(2, 2)
this.loader = new THREE.TextureLoader()
const image = await (new Promise(resolve => this.loader.load(this.props.imageSrc, resolve)))
image.wrapS = THREE.RepeatWrapping
image.wrapT = THREE.RepeatWrapping
this.uniforms = {
uTime: {
type: "f",
value: 1
},
uResolution: {
type: "v2",
value: new THREE.Vector2()
},
uMouse: {
type: "v2",
value: new THREE.Vector2()
},
uImage: {
value: image
},
uGridWidth: {
value: 0
},
uMagRadius: {
value: this.props.scopeSize
},
uFilterEnabled: {
value: this.props.filterEnabled
},
uShowScope: {
value: this.props.showScope
}
}
const material = new THREE.ShaderMaterial({
uniforms: this.uniforms,
vertexShader: this.props.vertexShader,
fragmentShader: this.props.fragmentShader
})
const mesh = new THREE.Mesh(geometry, material)
scene.add(mesh)
const renderer = new THREE.WebGLRenderer({
antialias: false,
alpha: false,
canvas: this.canvas
})
// renderer.setPixelRatio(window.devicePixelRatio)
this.handleWindowResize = this.onWindowResize(camera, renderer)
this.handleWindowResize(this.props.tileSize)
window.addEventListener('resize', () => this.handleWindowResize(this.props.tileSize), false)
const handleCursor = (e) => {
this.uniforms.uMouse.value.x = e.clientX - this.canvas.offsetLeft // * window.devicePixelRatio
this.uniforms.uMouse.value.y = this.canvas.height - (e.clientY - this.canvas.offsetTop) // * window.devicePixelRatio
}
this.canvas.onmousemove = e => handleCursor(e)
this.canvas.ontouchstart = this.canvas.ontouchmove = (e) => {
handleCursor(e.targetTouches[0])
e.preventDefault()
}
this.uniforms.uMouse.value = new THREE.Vector2(this.canvas.width / 2, this.canvas.height / 2)
this.animate(renderer, scene, camera)
}
async componentWillReceiveProps (nextProps) {
if (nextProps.imageSrc !== this.props.imageSrc) {
const image = await (new Promise(resolve => this.loader.load(nextProps.imageSrc, resolve)))
image.wrapS = THREE.RepeatWrapping
image.wrapT = THREE.RepeatWrapping
this.uniforms.uImage.value = image
this.handleWindowResize(nextProps.tileSize)
} else if (nextProps.tileSize !== this.props.tileSize) {
this.handleWindowResize(nextProps.tileSize)
}
if (nextProps.scopeSize !== this.props.scopeSize) {
this.uniforms.uMagRadius.value = nextProps.scopeSize
}
if (nextProps.showScope !== this.props.showScope) {
this.uniforms.uShowScope.value = nextProps.showScope
}
if (nextProps.filterEnabled !== this.props.filterEnabled) {
this.uniforms.uFilterEnabled.value = nextProps.filterEnabled
}
}
animate (renderer, scene, camera) {
requestAnimationFrame(() => {
this.animate(renderer, scene, camera, this.uniforms)
})
this.uniforms.uTime.value += 0.05
renderer.render(scene, camera)
}
onWindowResize (camera, renderer) {
return (tileSize) => {
// const size = Math.min(window.innerWidth, window.innerHeight)
const { image } = this.uniforms.uImage.value
const aspect = image.width / image.height
const width = renderer.domElement.parentElement.offsetHeight * aspect
const height = renderer.domElement.parentElement.offsetHeight
renderer.setSize(width, height)
this.uniforms.uResolution.value.x = renderer.domElement.width
this.uniforms.uResolution.value.y = renderer.domElement.height
this.uniforms.uGridWidth.value = width / tileSize
}
}
render () {
return (
<canvas ref={c => this.canvas = c} />
)
}
}