Skip to content

Instantly share code, notes, and snippets.

@roipeker
Last active April 26, 2018 09:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save roipeker/4dc0318b7eb371187bcd8dba2942bec8 to your computer and use it in GitHub Desktop.
Save roipeker/4dc0318b7eb371187bcd8dba2942bec8 to your computer and use it in GitHub Desktop.
Google Material Shadows for Starling.
// =================================================================================================
//
// Created by Rodrigo Lopez [roipeker™] on 26/04/2018.
//
// =================================================================================================
package roipeker.display.material {
// Values (in dp) based on:
// https://material.io/guidelines/material-design/elevation-shadows.html
public class Elevations {
public static const flat:uint = 0;
public static const appBar:uint = 4;
public static const raisedButton:uint = 2;
public static const raisedButtonPressed:uint = 8;
public static const fab:uint = 6;
public static const fabPressed:uint = 12;
public static const card:uint = 2;
public static const cardPressed:uint = 8;
public static const menus:uint = 8;
public static const submenus:uint = 9; // + sub menu level
public static const dialogs:uint = 24;
public static const navDrawer:uint = 16;
public static const drawer:uint = 16;
public static const modalBottomSheet:uint = 16;
public static const refreshIndicator:uint = 3;
// also quick entry
public static const searchBar:uint = 2;
public static const searchBarScrolled:uint = 3;
public static const snackBar:uint = 6; // toast alert.
public static const switchUI:uint = 1; // toast alert.
// -- Desktop --
public static const raisedButtonDesktop:uint = 0;
public static const raisedButtonHoverDesktop:uint = 2;
public static const cardDesktop:uint = 0;
public static const cardHoverDesktop:uint = 8;
public function Elevations() {
}
}
}
// =================================================================================================
//
// Created by Rodrigo Lopez [roipeker™] on 21/04/2018.
//
// =================================================================================================
package roipeker.display.material {
import flash.display3D.Context3DTextureFormat;
import starling.display.DisplayObject;
import starling.filters.DropShadowFilter;
import starling.filters.FilterChain;
import starling.utils.MathUtil;
/**
* ** Material Shadows **
*
* Reference for Material Shadows:
* https://material.io/guidelines/material-design/elevation-shadows.html
*
* The used values are based on Google's Photoshop references:
* https://material.io/guidelines/resources/shadows.html
*
* This class applies the 2 basic shadows required for any google material (key shadow and ambient shadow).
* MaterialShadow uses "composition", there's no need to extend it to create custom UI.
* A MaterialShadow requires a target and an elevation (z index)... It keeps current filters from the target object,
* even when the target object is "detached" from the MaterialShadow object (changed ::target or disposed).
*
* A MaterialShadow can be assigned only to 1 target at a time. But, the target can be changed at runtime, allows you to "reuse"
* instances when required.
*
* Even if filters in general (and particularly DropShadow) are heavy on the CPU and requires several "passes", therefore drawcalls,
* since Starling 2.0, the performance gain is great... so, the "impact" is for a few frames only.
*
* The Elevation class helps you with some shadows values.
*
* Sample code:
var card:Quad = new Quad(100, 100, 0xfafafa ) ;
card.x = card.y = 100;
addChild(card);
var fabButton:Canvas = new Canvas() ;
fabButton.beginFill(0x009688);
fabButton.drawCircle(0,0,50);
fabButton.endFill();
fabButton.y = 150 ;
fabButton.x = 250;
addChild(fabButton);
// ** pre-assigned filters are maintained
//card.filter = new GlowFilter( 0x00ff00, .5, .4, .4 );
var cardShadow:MaterialShadow = new MaterialShadow(card, .75);
cardShadow.elevation = 1 ;
card.addEventListener(TouchEvent.TOUCH, function(e:TouchEvent){
var t:Touch = e.getTouch( card );
if( t ){
if( t.phase == TouchPhase.BEGAN ){
Starling.juggler.tween( cardShadow, .3, { elevation: 6 });
} else if( t.phase == TouchPhase.ENDED){
Starling.juggler.tween( cardShadow, .4, { elevation: 1 });
}
}
});
// ** change targets at runtime.
Starling.juggler.delayCall( function(){
cardShadow.target = fabButton ;
}, 5 );
// ** dispose when it's not required
// cardShadow.dispose();
*
*/
public class MaterialShadow {
private static const MATERIAL_DIST_BLUR:Array =
[
[[0, 0], [0, 0]], // 0
[[2, 3], [2, 4]], // 1
[[3, 5], [1, 7]], // 2
[[5, 7], [2, 10]], // 3
[[7, 10], [3, 13]], // 4
[[8.5, 12.5], [3.5, 16.5]], // *5
[[10, 15], [4, 20]], // 6
[[11.5, 17.5], [4.5, 23.5]], // *7
[[13, 20], [5, 27]], // 8
[[18, 27], [9, 36]], // 9
[[18.6, 28], [8.6, 37.6]], // *10
[[19.3, 29], [8.3, 39.3]], // *11
[[20, 30], [8, 41]], // 12
[[22, 32.5], [8.75, 44]], // *13
[[24, 35], [9.5, 47]], // *14
[[26, 37.5], [10.25, 50]], // *15
[[28, 40], [11, 53]], // 16
[[29.3, 42.5], [11.5, 56.5]], // *17
[[30.7, 45], [12, 60]], // *18
[[32.1, 47.5], [12.5, 63.5]], // *19
[[33.5, 50], [13, 67]], // *20
[[34.8, 52.5], [13.5, 70.5]], // *21
[[36.2, 55], [14, 74]], // *22
[[37.6, 57.5], [14.5, 77.5]], // *23
[[39, 60], [15, 81]] // 24
];
private var _target:DisplayObject;
// elevation in dps. (range from 0-24)
private var _elevation:Number = 0;
private var _chain:FilterChain;
private var _dp1:DropShadowFilter;
private var _dp2:DropShadowFilter;
private var _quality:Number = 0.5;
/**
* Constructor.
* @param target
* @param quality
*/
public function MaterialShadow(target:DisplayObject, quality:Number = .6) {
_quality = quality;
_target = target;
init();
}
private function init():void {
const ANGLE:Number = Math.PI / 2;
_dp1 = new DropShadowFilter(0, ANGLE, 0x0, 0.24, 0, _quality);
_dp2 = new DropShadowFilter(0, ANGLE, 0x0, 0.16, 0, _quality);
_chain = new FilterChain(_dp1, _dp2);
_dp1.textureFormat = Context3DTextureFormat.BGRA_PACKED;
_dp2.textureFormat = Context3DTextureFormat.BGRA_PACKED;
if (_target) {
if (_target.filter) {
_chain.addFilter(_target.filter);
}
_target.filter = _chain;
}
}
/**
* Toggles the filter cache to optimize rendering
* @param flag
*/
public function cache(flag:Boolean):void {
if (_chain.isCached == flag) return;
flag ? _chain.cache() : _chain.clearCache();
}
/**
* Disposes the MaterialShadow object.
*/
public function dispose():void {
if (_target) {
if (_chain.numFilters > 2) {
_target.filter = _chain.removeFilterAt(-1, false);
} else {
_target.filter = null;
}
}
_target = null;
_chain.dispose();
_chain = null;
_dp1 = null;
_dp2 = null;
}
private function applyMod(dp:DropShadowFilter, distance:Number, blur:Number):void {
var b:Number = blur / 10 * _quality;
if (b <= 0) {
dp.alpha = 0;
} else {
dp.distance = distance / 2;
dp.blur = b;
}
}
public function set target(value:DisplayObject):void {
if (_target == value) return;
// remove filter from previous target.
if (_target) {
if (_chain.numFilters > 2) {
_target.filter = _chain.removeFilterAt(-1, false);
} else {
_target.filter = null;
}
}
_target = value;
if (_target) {
if (_target.filter) {
_chain.addFilter(_target.filter);
}
_target.filter = _chain;
}
}
public function get target():DisplayObject {
return _target;
}
public function get elevation():Number {
return _elevation;
}
public function set elevation(value:Number):void {
// clamp value in range.
value = MathUtil.clamp(value, 0, 24);
// to reduce a CPU when possible, negligible visual difference.
if (Math.abs(_elevation - value) < .3) {
return;
}
_elevation = value;
if (value != int(value)) {
// calculate the shadow interval.
var id1:int = Math.floor(value);
var id2:int = Math.ceil(value);
var p:Number = value - id1;
var o1:Array = MATERIAL_DIST_BLUR[id1];
var o2:Array = MATERIAL_DIST_BLUR[id2];
var d1:Number = o1[0][0] + (o2[0][0] - o1[0][0]) * p;
var b1:Number = o1[0][1] + (o2[0][1] - o1[0][1]) * p;
var d2:Number = o1[1][0] + (o2[1][0] - o1[1][0]) * p;
var b2:Number = o1[1][1] + (o2[1][1] - o1[1][1]) * p;
} else {
d1 = MATERIAL_DIST_BLUR[value][0][0];
b1 = MATERIAL_DIST_BLUR[value][0][1];
d2 = MATERIAL_DIST_BLUR[value][1][0];
b2 = MATERIAL_DIST_BLUR[value][1][1];
}
_dp1.alpha = .24;
_dp2.alpha = .16;
applyMod(_dp1, d1, b1);
applyMod(_dp2, d2, b2);
}
public function get quality():Number {
return _quality;
}
public function set quality(value:Number):void {
if (_quality == value) return;
_quality = value;
_dp1.quality = _dp2.quality = value;
// change quality, refresh the blur radius accordingly.
var e:Number = _elevation;
_elevation = 30; // invalid number to force render.
elevation = e;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment