Last active
April 26, 2018 09:45
-
-
Save roipeker/4dc0318b7eb371187bcd8dba2942bec8 to your computer and use it in GitHub Desktop.
Google Material Shadows for Starling.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ================================================================================================= | |
// | |
// 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() { | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ================================================================================================= | |
// | |
// 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