Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add motion background block #19

Merged
merged 50 commits into from
Mar 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
b5b63cb
Add template for WebGL block
ajlende Jan 14, 2020
02c518d
Update script hook to show in the editor too
ajlende Jan 17, 2020
c12ee5a
Add 'paint' background shader
ajlende Jan 21, 2020
7618277
Update twgl.js and include sourcemaps and types for debugging
ajlende Feb 3, 2020
148142d
Refactor and add custom gradient
ajlende Feb 7, 2020
7496e4f
Fix performance on creating framebuffers
ajlende Feb 7, 2020
80f500a
Adjust where the canvas is inserted
ajlende Feb 7, 2020
8a4c34f
Remove debugging parts from shaders
ajlende Feb 7, 2020
9a24e4a
Fix canvas positioning at 600px
ajlende Feb 7, 2020
202a309
Remove unused css
ajlende Feb 7, 2020
6edda57
Clean up shaders
ajlende Feb 7, 2020
c1a5a89
Update UI for custom colors
ajlende Feb 7, 2020
099e6c7
Update fragment shader effect source comment
ajlende Feb 7, 2020
2717a82
Remove allowReset from range sliders
ajlende Feb 7, 2020
be4cc91
Refactor for readability
ajlende Feb 7, 2020
3a12327
Fix effect stretching
ajlende Feb 7, 2020
bc7b525
Initialize rgb values in case the color string is invalid
ajlende Feb 7, 2020
fb29c86
Add support for image upload
ajlende Feb 7, 2020
60b1bc1
Update UI for image select
ajlende Feb 7, 2020
f83e117
Fix z-index issue
ajlende Feb 7, 2020
5f3d7b0
Remove default color values
ajlende Feb 7, 2020
5b03f86
Fix media placeholder overlay on gradient
ajlende Feb 7, 2020
35d37a1
Fix blocks sticking to screen when scrolling fast
ajlende Feb 7, 2020
3ca9a69
Fix canvas not appearing when adding first block
ajlende Feb 7, 2020
f9525a5
Add URL image select
ajlende Feb 7, 2020
ae43d91
Add accept and allowedTypes for image
ajlende Feb 7, 2020
b627987
Fix memory leak
ajlende Feb 7, 2020
9cf6d57
Optimize the parseColor function
ajlende Feb 12, 2020
6eeaa1a
Update default colors to be darker
ajlende Feb 12, 2020
69c446f
Code cleanup
ajlende Feb 12, 2020
aa12468
Set background for image placeholder
ajlende Feb 12, 2020
b54d39b
Add text heading
ajlende Feb 12, 2020
a147bb0
Add block.json for block directory
ajlende Feb 12, 2020
6b53caf
Fix image replace not changing
ajlende Feb 14, 2020
f2b45f9
Fix webgl context in IE11
ajlende Feb 14, 2020
22171df
Fix image placeholder CSS
ajlende Feb 14, 2020
927cdf8
Replace for...of with lodash forEach for IE support
ajlende Mar 3, 2020
c1ed359
Fix device preview background
ajlende Mar 3, 2020
1c6a054
Try canvas container block
ajlende Mar 5, 2020
d03b0b5
Fix block positioning within canvas
ajlende Mar 10, 2020
b93974b
Temporarily use supports align to change canvas size
ajlende Mar 10, 2020
958e7da
Remove console log
ajlende Mar 10, 2020
b6b56ed
Make debug cursor more visible
ajlende Mar 10, 2020
b380736
Fix eslint on math expression
ajlende Mar 10, 2020
2c169d0
Fix cursor position
ajlende Mar 10, 2020
b5b692b
Add lodash frontend dependency
ajlende Mar 10, 2020
77ce72f
Fix left positioning in device preview mode
ajlende Mar 10, 2020
903ee8a
Remove debug cursor code
ajlende Mar 10, 2020
1bf50b9
Rename container to canvas provider
ajlende Mar 10, 2020
aa3e26b
Fix vertex shader compiler error
ajlende Mar 10, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions blocks/motion-background/editor.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/* Editor styles */

.wp-block-a8c-motion-background {
display: flex;
.components-placeholder.components-placeholder {
position: absolute;
height: 100%;
width: 100%;
margin-bottom: 0;
z-index: 1;
}
}

.components-button-group.is-radio-group {
display: flex;
margin-bottom: 0.5em;

.components-button {
display: inline-block;
flex-grow: 1;
}
}
4 changes: 4 additions & 0 deletions blocks/motion-background/index.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[
"twgl/twgl.js",
"motion-background.js"
]
26 changes: 26 additions & 0 deletions blocks/motion-background/index.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

add_action( 'init', function() {
register_block_type( 'a8c/motion-background', [
'editor_script' => 'wpcom-blocks',
'style' => 'wpcom-blocks',
'editor_style' => 'wpcom-blocks-editor',
] );
} );

add_action( 'enqueue_block_assets', function() {
wp_enqueue_script(
'wpcom-twgl-js',
plugins_url( 'twgl/twgl.js', __FILE__ ),
[], // no dependencies
filemtime( plugin_dir_path( __FILE__ ) . 'twgl/twgl.js' ),
true // in footer
);
wp_enqueue_script(
'wpcom-motion-background-js',
plugins_url( 'motion-background.js', __FILE__ ),
[ 'wpcom-twgl-js', 'lodash' ],
filemtime( plugin_dir_path( __FILE__ ) . 'motion-background.js' ),
true // in footer
);
} );
325 changes: 325 additions & 0 deletions blocks/motion-background/motion-background.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,325 @@
/**
* Shaders based on fluid effect from {@link http://glslsandbox.com/e#8143.0}
* Draws multiple blocks on a canvas using a technique from {@link https://webglfundamentals.org/webgl/lessons/webgl-multiple-views.html}
*/

( function( _, twgl ) {
if ( document.getElementById( 'editor' ) ) {
return; // TODO: disable this properly rather than with this return
}

const blocks = document.getElementsByClassName( 'wp-block-a8c-motion-background' );

const gl = twgl.getWebGLContext( document.createElement( 'canvas' ) );
gl.canvas.className = 'wp-block-a8c-motion-background-canvas';
if ( ! gl ) {
_.forEach( blocks, ( block ) => {
// TODO: I18n
block.textContent = 'WebGL must be enabled to view this content.';
} );
return;
}

document.body.insertBefore( gl.canvas, document.body.firstChild );

const vertexShader = `
attribute vec3 position;
attribute vec2 texcoord;

varying vec2 uv;

void main () {
uv = texcoord;
gl_Position = vec4( position, 1. );
}
`;

const fragmentShader = `
precision mediump float;

#define SRGB_TO_LINEAR( c ) pow( c, vec3( 2.2 ) )
#define LINEAR_TO_SRGB( c ) pow( c, vec3( 1.0 / 2.2 ) )

uniform vec3 color1;
uniform vec3 color2;
uniform vec3 color3;
uniform vec3 color4;

varying vec2 uv;

void main()
{
vec3 tl = SRGB_TO_LINEAR( color1 );
vec3 tr = SRGB_TO_LINEAR( color2 );
vec3 bl = SRGB_TO_LINEAR( color3 );
vec3 br = SRGB_TO_LINEAR( color4 );

vec3 top = mix( tl, tr, uv.x );
vec3 bottom = mix( bl, br, uv.x );
vec3 gradient = mix( bottom, top, uv.y ) ;

gl_FragColor = vec4( LINEAR_TO_SRGB( gradient ), 1. );
}
`;

const vertexShaderEffect = `
attribute vec3 position;
attribute vec2 texcoord;

uniform vec2 resolution;

varying vec2 uv;

void main () {
uv = texcoord * normalize( resolution );
gl_Position = vec4( position, 1. );
}
`;

const fragmentShaderEffect = `
precision mediump float;

#define MAX_COMPLEXITY 32
#define MIRRORED_REPEAT( p ) abs( 2. * fract( p / 2. ) - 1. )

uniform float time;

uniform vec2 mouse;
uniform int complexity;
uniform float mouseSpeed;
uniform float fluidSpeed;
uniform sampler2D texture;

varying vec2 uv;

void main() {
vec2 c = uv;
for ( int i = 1; i < MAX_COMPLEXITY; i++ ) {
if ( i >= complexity ) continue;
float f = float( i );
c += ( time * 0.001 );
c.x += 0.6 / f * sin( ( f * c.y ) + ( time / fluidSpeed ) + ( 0.3 * f ) );
c.x += 0.5 + ( mouse.y / mouseSpeed );
c.y += 0.6 / f * sin( ( f * c.x ) + ( time / fluidSpeed ) + ( 0.3 * f + 10. ) );
c.y -= 0.5 - ( mouse.x / mouseSpeed );
}
gl_FragColor = texture2D( texture, MIRRORED_REPEAT( c ) );
}
`;

const programInfoGradient = twgl.createProgramInfo( gl, [ vertexShader, fragmentShader ] );
const programInfoEffectPass = twgl.createProgramInfo( gl, [ vertexShaderEffect, fragmentShaderEffect ] );

const screenBufferInfo = twgl.createBufferInfoFromArrays( gl, {
position: [ -1, -1, 0, 1, -1, 0, -1, 1, 0, -1, 1, 0, 1, -1, 0, 1, 1, 0 ], // vec3
texcoord: [ 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1 ], // vec2
} );

const textureInfos = new WeakMap();

const globalState = {
time: window.performance.now(),
mouse: [ 0, 0 ],
};

/**
* Manage rendering all of the blocks by calculating the position of each
* div and only rendering the blocks on screen. The position information is
* sent to the render functions to allow them to do a viewport change and
* scissor test for that specific block.
*/
function renderAllBlocks() {
twgl.resizeCanvasToDisplaySize( gl.canvas );

// Move the canvas to top of the current scroll position without jittering
gl.canvas.style.transform = `translateY(${ window.scrollY }px)`;

gl.enable( gl.CULL_FACE );
gl.enable( gl.DEPTH_TEST );
gl.enable( gl.SCISSOR_TEST );

gl.viewport( 0, 0, gl.canvas.width, gl.canvas.height );
gl.scissor( 0, 0, gl.canvas.width, gl.canvas.height );
gl.clearColor( 0, 0, 0, 0 );
gl.clear( gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT ); // eslint-disable-line no-bitwise

_.forEach( blocks, ( block ) => {
const rect = block.getBoundingClientRect();

if (
rect.bottom < 0 ||
rect.top > gl.canvas.clientHeight ||
rect.right < 0 ||
rect.left > gl.canvas.clientWidth
) {
return; // Block is off screen
}

const width = rect.right - rect.left;
const height = rect.bottom - rect.top;
const left = rect.left;
const bottom = gl.canvas.clientHeight - rect.bottom;

renderBlock( block, [ width, height ], [ left, bottom ] );
} );
}

/**
* Draw an individual block.
*
* @param {Node} block Block's DOM Node
* @param {number[]} resolution Pixel [ width, height ] resolution of the block
* @param {number[]} offset Pixel [ left, bottom ] offset of the block
*/
function renderBlock( block, resolution, offset ) {
let textureInfo = textureInfos.get( block );

if (
! textureInfo ||
textureInfo.mode !== block.dataset.mode ||
textureInfo.imageUrl !== block.dataset.imageUrl
) {
if ( block.dataset.mode === 'image' ) {
// Default to semi-transparent placeholder color
const src = block.dataset.imageUrl || [ 0.55, 0.55, 0.59, 0.1 ];
// Match the structure of what createFramebufferInfo makes for the WebGLTexture
textureInfo = {
mode: block.dataset.mode,
imageUrl: block.dataset.imageUrl,
attachments: [ twgl.createTexture( gl, { src } ) ],
};
} else if ( block.dataset.mode === 'gradient' ) {
// A 512x512 resolution seemed to have a smooth enough gradient for the size of blocks
textureInfo = twgl.createFramebufferInfo( gl, null, 512, 512 );
textureInfo.mode = block.dataset.mode;
textureInfos.set( block, textureInfo );
}
textureInfos.set( block, textureInfo );
}

if ( block.dataset.mode === 'gradient' ) {
renderGradient( block.dataset, textureInfo );
}
renderLiquidEffect( block.dataset, resolution, offset, textureInfo );
}

/**
* Convert a hex color string to a WebGL color vector.
*
* @param {string} color Hex color string (#FFFFFF or #FFF)
* @return {number[]} RGB array for WebGL
*/
function parseColor( color ) {
let r = '0';
let g = '0';
let b = '0';

if ( color.length === 7 ) {
r = '0x' + color[ 1 ] + color[ 2 ];
g = '0x' + color[ 3 ] + color[ 4 ];
b = '0x' + color[ 5 ] + color[ 6 ];
} else if ( color.length === 4 ) {
r = '0x' + color[ 1 ] + color[ 1 ];
g = '0x' + color[ 2 ] + color[ 2 ];
b = '0x' + color[ 3 ] + color[ 3 ];
}

return [ r / 0xFF, g / 0xFF, b / 0xFF ];
}

/**
* @typedef {Object} FramebufferInfo
* @see {@link https://twgljs.org/docs/module-twgl.html#.FramebufferInfo}
* Either a twlg FramebufferInfo or an image texture in the same shape as
* a FramebufferInfo.
*/

/**
* Draw the custom gradient to the framebuffer.
*
* @param {Object} blockData Dataset from block
* @param {FramebufferInfo} textureInfo Framebuffer info from twgl
*/
function renderGradient( blockData, textureInfo ) {
const uniforms = {
color1: parseColor( blockData.color1 || '#F00' ),
color2: parseColor( blockData.color2 || '#0F0' ),
color3: parseColor( blockData.color3 || '#000' ),
color4: parseColor( blockData.color4 || '#00F' ),
};

twgl.bindFramebufferInfo( gl, textureInfo );
gl.viewport( 0, 0, textureInfo.width, textureInfo.height );
gl.scissor( 0, 0, textureInfo.width, textureInfo.height );
gl.clearColor( 1, 1, 1, 1 );
gl.clear( gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT ); // eslint-disable-line no-bitwise
gl.useProgram( programInfoGradient.program );
twgl.setBuffersAndAttributes( gl, programInfoGradient, screenBufferInfo );
twgl.setUniforms( programInfoGradient, uniforms );
twgl.drawBufferInfo( gl, screenBufferInfo );
}

/**
* Draw the liquid effect to the canvas.
*
* @param {Object} blockData Dataset from block
* @param {number[]} resolution Pixel [ width, height ] resolution of the block
* @param {number[]} offset Pixel [ left, bottom ] offset of the block
* @param {FramebufferInfo} textureInfo Framebuffer info from twgl
*/
function renderLiquidEffect( blockData, resolution, offset, textureInfo ) {
const complexity = Number.parseInt( blockData.complexity, 10 );
const mouseSpeed = Number.parseFloat( blockData.mouseSpeed );
const fluidSpeed = Number.parseFloat( blockData.fluidSpeed );

const uniforms = {
// Required in the vertex shader to prevent stretching
resolution,
// Time since beginning of the program in seconds
time: globalState.time * 0.001,
// Mouse position in normalized block coordinates
mouse: globalState.mouse.map( ( v, i ) => ( v - offset[ i ] ) / resolution[ i ] ),
// 'Swirly-ness' of the effect
complexity,
// Makes it more/less jumpy. f(x) from [1, 100] to [50, 1]
mouseSpeed: ( 700 + ( 4 * mouseSpeed ) ) / ( 11 * mouseSpeed ) * complexity,
// Drives speed, higher number will make it slower. f(x) from [1, 100] to [256, 1]
fluidSpeed: ( 8500 - ( 52 * fluidSpeed ) ) / ( 33 * fluidSpeed ),
// Framebuffer texture from the first pass
texture: textureInfo.attachments[ 0 ],
};

twgl.bindFramebufferInfo( gl, null ); // Draw to screen
gl.viewport( ...offset, ...resolution );
gl.scissor( ...offset, ...resolution );
gl.clearColor( 0, 0, 0, 0 );
gl.clear( gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT ); // eslint-disable-line no-bitwise
gl.useProgram( programInfoEffectPass.program );
twgl.setBuffersAndAttributes( gl, programInfoEffectPass, screenBufferInfo );
twgl.setUniforms( programInfoEffectPass, uniforms );
twgl.drawBufferInfo( gl, screenBufferInfo );
}

/**
* Update mouse globals.
*
* @param {MouseEvent} e Mouse event
*/
function updateMouse( e ) {
globalState.mouse[ 0 ] = e.clientX;
globalState.mouse[ 1 ] = gl.canvas.height - e.clientY; // From bottom
}
document.body.addEventListener( 'mousemove', updateMouse );

/**
* Run the animation loop.
*
* @param {DOMHighResTimeStamp} t Point in time when function begins to be called in milliseconds
*/
function animate( t ) {
window.requestAnimationFrame( animate );
globalState.time = t;
renderAllBlocks();
}
window.requestAnimationFrame( animate );
}( window.lodash, window.twgl ) );
Loading