Skip to content

Instantly share code, notes, and snippets.

@AndreiRudenko
Last active July 24, 2016 20:19
Show Gist options
  • Save AndreiRudenko/6ebff39903a6ef83926259c88944338b to your computer and use it in GitHub Desktop.
Save AndreiRudenko/6ebff39903a6ef83926259c88944338b to your computer and use it in GitHub Desktop.
SpriteRenderer, VisualRenderer, and SpriteAnimation Components for Luxeengine
import luxe.Component;
import luxe.Rectangle;
import luxe.Vector;
// import luxe.Sprite;
import phoenix.Texture;
import luxe.Log.*;
/** A component for : luxe.Sprite
Once attached to a sprite, the animation component can play animations,
fire events on specific frames, and handles both UV (spritesheet/atlas)
animation types or image sequences.
*/
class SpriteAnimation extends Component {
/** The current animation data for this animation */
public var current : SpriteAnimationData;
/** The current animation frame information */
public var current_frame : SpriteAnimationFrame;
/** The list of animations added to this component */
public var animations : Map<String, SpriteAnimationData>;
/** The image frame of the current animation frame */
public var image_frame : Int = 0;
/** Whether or not the animation is playing.
Currently read only (in concept), manipulated by play() and stop().
This will be clarified in alpha-3.0 */
public var playing : Bool = false;
/** Whether or not the animation will loop at reaching an end point. */
public var loop : Bool = false;
/** Whether or not the animation is run in reverse when reaching an end point. */
public var pingpong : Bool = false;
/** Whether or not the animation is run in reverse. */
public var reverse : Bool = false;
/** The current active animation.
Will set the new animation frame to 1,
change the frame property if requiring a specific frame. */
@:isVar public var animation (get,set) : String;
/** The speed at which the animation plays.
This is in frames per second. */
@:isVar public var speed (get,set) : Float = 30.0;
/** The current animation frame (index into the frame set).
Setting this will update the sprite frame at any time. */
@:isVar public var frame (get,set) : Int = 1;
var time : Float = 0;
var next_frame_time : Float = 0;
var sprite : SpriteRenderer;
var uv_cache : Rectangle;
/** Create a new `SpriteAnimation` */
public function new( options:luxe.options.ComponentOptions ) {
super(options);
animations = new Map();
uv_cache = new Rectangle();
} //new
//component
override function onadded() {
sprite = get('SpriteRenderer');
assertnull(sprite, 'SpriteAnimation need SpriteRenderer component');
} // onadded
override function init() {
} //init
//public playback API :todo: these are a bit odd.
/** Reset the frame to 1 and `play`. */
public function restart() : Void {
frame = 1;
play();
} //restart
/** Play/resume playback. */
public function play() : Void {
playing = true;
} //play
/** Stop/pause playback. Does not (currently) reset the animation to frame 1. */
public function stop() : Void {
playing = false;
} //stop
//public animation data API
/** Add an animation object from a raw `SpriteAnimationData` object. */
public function add_from_anim_data( _data:SpriteAnimationData ) {
animations.set(_data.name, _data);
} //add_from_anim_data
/** Add an animation object from a string containing JSON data. */
public function add_from_json_object( _json_object : Dynamic ) : Void {
var anim_items = _json_object;
var anims = Reflect.fields(anim_items);
if(anims.length > 0) {
for(anim in anims) {
var animdata : Dynamic = Reflect.field(anim_items, anim);
var _anim = new SpriteAnimationData( sprite, anim );
_anim.from_json( animdata );
animations.set( anim, _anim );
} //anim in anims
} else { //anims.length > 0
log('${entity.name} / add_from_json_object given an empty json object... This is probably an error.');
}
} //add_from_json_object
/** Add an animation object from a string containing JSON data. */
public function add_from_json( _json_string : String ) : Void {
//
//parse json first
var _json_object = haxe.Json.parse(_json_string);
//and add directly
add_from_json_object( _json_object );
} //add_from_json
//public event api
/** Remove a specific event from a frame */
public function remove_event( _animation:String, _image_frame:Int, _event:String='' ) : Void {
if( animations.exists(_animation) ) {
var _anim = animations.get(_animation);
for(_anim_frame in _anim.frameset) {
if( _anim_frame.image_frame != _image_frame ) continue;
for(_frame_event in _anim_frame.events) {
if(_frame_event.frame != _image_frame) continue;
if(_frame_event.event != _event) continue;
_anim_frame.events.remove(_frame_event);
_debug("anim event being removed " + _image_frame + ":" + _event + " to " + _animation);
} //each event
} //each frame in the set
} else {
log('${entity.name} / $animation requested an event to be added, but that animation is not found in the `$name` component');
}
} //remove_event
/** Remove all events from a frame */
public function remove_events( _animation:String, _image_frame:Int) : Void {
if( animations.exists(_animation) ) {
var _anim = animations.get(_animation);
for(_anim_frame in _anim.frameset) {
if( _anim_frame.image_frame == _image_frame ) {
//clear the events
_anim_frame.events = [];
} //matched frame index
} //each frame in the set
} else {
log('${entity.name} / $animation requested an event to be removed, but that animation is not found in the `$name` component');
}
} //remove_events
public function add_event( _animation:String, _image_frame:Int, _event:String='' ) : Void {
_debug("\n adding event to animation : " + _animation + " " + _image_frame + ":" + _event);
if( animations.exists(_animation) ) {
var _anim = animations.get(_animation);
_debug("animation: found anim " + _animation + ", looking for event duplicates ");
for(_anim_frame in _anim.frameset) {
if( _anim_frame.image_frame == _image_frame ) {
//matched image frame, now check if there isn't already an event named this way in the list
//so that we don't have multiples causing issues
var _add_event : Bool = true;
for(_frame_event in _anim_frame.events) {
//found an event for this frame, is it the same one?
if(_frame_event.frame == _image_frame && _frame_event.event == _event) {
_add_event = false;
_debug("anim event duplicate, not adding " + _image_frame + ":" + _event + " to " + _animation);
}
}
//still?
if(_add_event) {
_anim_frame.events.push({ frame:_image_frame, event:_event });
_debug("anim event added : " + _image_frame + ":" + _event + " to " + _animation);
}
} //if the frame matches
} //for the frameset in
} else {
log('${entity.name} / $animation requested an event to be added, but that animation is not found in the `$name` component');
} //
} //add_event
//Internal
@:noCompletion override function update( dt:Float ) : Void {
if(current == null) {
// trace('SpriteAnimation: ignoring update when current is null');
return;
}
if(!playing) return;
//flag if we reached an end state
var end = false;
var _frame = frame;
//update the local time
time += dt;
//time for a frame update?
if(time >= next_frame_time) {
next_frame_time = time + current.frame_time;
//advance the frame
if(!reverse) {
_frame += 1;
} else {
_frame -= 1;
}
//check the logic for syncing
if( !reverse ) {
if( _frame > current.frame_count ) {
end = true;
if(!loop) {
_frame = current.frame_count;
}
} //if at the end
} else {
if( _frame < 1 ) {
end = true;
if(!loop) {
_frame = 1;
}
} //frame < 1
} //!reverse
//check the end status
if(end) {
if(loop) {
if(pingpong) {
reverse = !reverse;
}
if(!reverse) {
_frame = 1;
} else {
_frame = current.frame_count;
}
emit_anim_event('loop');
} else {
stop();
emit_anim_event('end');
_frame = frame;
}
} //if end
//sync up the actual frame with the updated value
frame = _frame;
} //time?
} //update
//sync the state to the sprite itself
function refresh_sprite() : Void {
assertnull(sprite, 'SpriteAnimation requires non-null sprite instance');
if(current.type == SpriteAnimationType.animated_uv) {
assertnull(sprite.texture, 'SpriteAnimation with animated_uv type requires a texture that is not null');
//cache the uv so we don't allocate for no good reason
uv_cache.set( current_frame.frame_source.x, current_frame.frame_source.y, current_frame.frame_source.w, current_frame.frame_source.h );
//ratio of scale between sprite size and frame size
var _ratio_x = current_frame.frame_size.x / sprite.size.x;
var _ratio_y = current_frame.frame_size.y / sprite.size.y;
//resize the sprite non destructively, to fit the new frame size
sprite.geometry.transform.scale.x = (current_frame.frame_source.w / (current_frame.frame_size.x)) * sprite.scale.x;
sprite.geometry.transform.scale.y = (current_frame.frame_source.h / (current_frame.frame_size.y)) * sprite.scale.y;
//realign the sprite to match the new frame size, but also adjust for the new scale! otherwise it won't match
sprite.geometry.transform.origin.x = -((current_frame.frame_pos.x / _ratio_x) * sprite.scale.x) / sprite.geometry.transform.scale.x;
sprite.geometry.transform.origin.y = -((current_frame.frame_pos.y / _ratio_y) * sprite.scale.y) / sprite.geometry.transform.scale.y;
//and finally assign it to the sprite
sprite.uv = uv_cache;
} else if(current.type == SpriteAnimationType.animated_texture) {
if( image_frame <= current.image_set.length ) {
sprite.texture = current.image_set[image_frame-1];
uv_cache.set( current_frame.frame_source.x, current_frame.frame_source.y, current_frame.frame_source.w, current_frame.frame_source.h );
sprite.uv = uv_cache;
} //image_frame inside image set
}
} //refresh_sprite
//properties
function get_frame() : Int {
return frame;
} //get_frame
function set_frame( _frame:Int ) : Int {
frame = _frame;
if(current == null) {
return frame;
}
//:todo: the frame events could be deferred
//and need testing but fixing quickly,
//investigate this properly
if(entity == null || entity.destroyed) {
return frame;
}
current_frame = current.frameset[ frame - 1 ];
image_frame = current_frame.image_frame;
emit_frame_events();
refresh_sprite();
return frame;
} //set_frame
inline function get_speed() : Float {
return speed;
} //get_speed
function set_speed( _speed:Float ) : Float {
//:todo: it may make more sense to scale this in the updates
//rather than modify the frame time like this
if(current != null) {
current.frame_time = 1 / _speed;
}
return speed = _speed;
} //set_speed
inline function get_animation() : String {
return animation;
} //get_animation
function set_animation( _name:String ) : String {
if(!animations.exists(_name)) {
log('${entity.name} / set animation `$_name`, but that animation is not found in the `$name` component');
return animation;
}
current = animations.get(_name);
loop = current.loop;
pingpong = current.pingpong;
reverse = current.reverse;
next_frame_time = time + current.frame_time;
//set to the first frame of this animation
frame = 1;
return animation = _name;
} //set_animation
//internal
inline function emit_frame_events() : Void {
//
//handle any frame events
for(_event in current_frame.events) {
var _event_emit_name : String = _event.event;
//default to animation.event.image_frame
if(_event_emit_name == '') {
_event_emit_name = 'animation.$animation.${current_frame.image_frame}';
}
//fire the event into the holding entity
entity.events.fire( _event_emit_name, {
animation : animation,
event: _event_emit_name,
frame_event : _event,
frame: current_frame,
image_frame : current_frame.image_frame
});
} //each event
} //emit_frame_events
inline function emit_anim_event(_name:String) {
var _event_emit_name = 'animation.$animation.$_name';
entity.events.fire( _event_emit_name, {
animation : animation,
event: _event_emit_name,
frame_event : null,
frame: current_frame,
image_frame : current_frame.image_frame
});
}
} //SpriteAnimation
typedef SpriteAnimationEventData = {
animation : String,
event: String,
frame_event : SpriteAnimationFrameEvent,
frame: SpriteAnimationFrame,
image_frame : Int
}
typedef SpriteAnimationFrameEvent = {
frame : Int,
event : String
}
typedef SpriteAnimationFrameSource = {
frame : Int,
source : Rectangle,
size : Vector,
pos : Vector
}
typedef SpriteAnimationFrame = {
image_frame : Int,
?image_source : Texture,
frame_source : Rectangle,
frame_size : Vector,
frame_pos : Vector,
events : Array<SpriteAnimationFrameEvent>
}
enum SpriteAnimationType {
animated_texture;
animated_uv;
}
class SpriteAnimationData {
public static var frame_range_regex : EReg = ~/(\d*)(\b\s*?-\s*?\b)(\d*)/gi;
public static var frame_hold_regex : EReg = ~/(\d*)(\shold\s)(\d*)/gi;
public static var frame_hold_prev_regex : EReg = ~/(\bhold\s)(\d*)/gi;
public static var frame_regex : EReg = ~/(\d*)/gi;
public var name : String;
public var type : SpriteAnimationType;
public var filter_type : Null<FilterType>;
public var frameset : Array<SpriteAnimationFrame>;
public var image_set_list : Array<String>;
public var image_set : Array<Texture>;
public var frame_size : Vector;
public var frame_sources : Array<SpriteAnimationFrameSource>;
public var frame_time : Float = 0.05;
public var loop : Bool = false;
public var pingpong : Bool = false;
public var reverse : Bool = false;
var sprite : SpriteRenderer;
@:isVar public var frame_count (get,never) : Int = 0;
function get_frame_count() {
return frameset.length;
}
public function new( _sprite:SpriteRenderer, ?_name:String = 'anim' ) {
name = _name;
sprite = _sprite;
frameset = [];
frame_sources = [];
frame_size = new Vector();
}
public function from_json( _animdata:Dynamic ) {
assertnull(_animdata, 'Null animation object passed to from_json in SpriteAnimation');
var _json_frameset : Array<String> = cast _animdata.frameset;
var _json_frame_size : Dynamic = _animdata.frame_size;
var _json_pingpong : Dynamic = _animdata.pingpong;
var _json_loop : Dynamic = _animdata.loop;
var _json_reverse : Dynamic = _animdata.reverse;
var _json_speed : Dynamic = _animdata.speed;
var _json_image_sequence : String = cast _animdata.image_sequence;
var _json_filter_type : String = cast _animdata.filter_type;
var _json_events_list : Array<Dynamic> = cast _animdata.events;
var _json_framesource_list : Array<Dynamic> = cast _animdata.frame_sources;
//frameset
assertnull(_json_frameset, 'SpriteAnimation passed invalid json, anim data requires frameset as an array of strings.');
var _frameset : Array<Int> = parse_frameset( _json_frameset );
type = SpriteAnimationType.animated_uv;
//filter type
if(_json_filter_type != null) {
switch (_json_filter_type) {
case 'nearest': filter_type = FilterType.nearest;
case 'linear': filter_type = FilterType.linear;
}
}
//frame_size
var _frame_size = new Vector();
if(_json_frame_size != null) {
var _x : Float = Std.parseFloat(_json_frame_size.x);
var _y : Float = Std.parseFloat(_json_frame_size.y);
_frame_size.set_xy(_x, _y);
}
//pingpong
var _pingpong : Bool = false;
if(_json_pingpong != null) {
if(_json_pingpong == 'true') {
_pingpong = true;
} else {
_pingpong = false;
}
} //_json_pingpong
//loop
var _loop : Bool = false;
if(_json_loop != null) {
if(_json_loop == 'true') {
_loop = true;
} else {
_loop = false;
}
} //_json_loop
//_reverse
var _reverse : Bool = false;
if(_json_reverse != null) {
if(_json_reverse == 'true') {
_reverse = true;
} else {
_reverse = false;
}
} //_json_loop
//speed
var _speed : Float = 2;
if(_json_speed != null) {
_speed = Std.parseFloat(_json_speed);
}
//events
var _events : Array<SpriteAnimationFrameEvent> = null;
if(_json_events_list != null) {
_events = parse_event_set(_json_events_list);
}
//frame_sources
//store the default frame size here so we can fill in blanks
//after we parse the sources
frame_size = _frame_size;
var _frame_sources : Array<SpriteAnimationFrameSource> = null;
if(_json_framesource_list != null) {
_frame_sources = parse_frame_sources_set(_json_framesource_list);
}
//create from the animation data
for( _frame in _frameset ) {
frameset.push({
image_frame : _frame,
events : parse_event_for_frame(_events,_frame),
frame_source : parse_source_for_frame(_frame_sources,_frame),
frame_size : parse_source_size_for_frame(_frame_sources, _frame),
frame_pos : parse_source_pos_for_frame(_frame_sources, _frame)
});
// trace("add frame : " + frameset[frameset.length-1]);
}
//image sequence
if(_json_image_sequence != null) {
//ask for the textures
var _images_list = Luxe.utils.find_assets_sequence( _json_image_sequence );
//set the type
type = SpriteAnimationType.animated_texture;
image_set = [];
var _textures = [];
if(_images_list.length > 0) {
image_set_list = _images_list;
for(_image in _images_list) {
var _texture = Luxe.resources.texture(_image);
assertnull(_texture, 'SpriteAnimation texture id was not found : $_image');
image_set.push(_texture);
}
//run over the frame sets, store their texture in the frame
for(_frame in frameset) {
if(_frame.image_frame <= image_set.length) {
_frame.image_source = image_set[_frame.image_frame-1];
if(filter_type != null) {
_frame.image_source.filter_min = _frame.image_source.filter_mag = filter_type;
} //if filter type is set
} //if within the frame image set
} //each frameset
} //_images_list
} //_json_image_sequence
pingpong = _pingpong;
loop = _loop;
reverse = _reverse;
frame_time = 1 / _speed;
return this;
} //from_json
function parse_event_for_frame( _events:Array<SpriteAnimationFrameEvent>, _frame:Int ) : Array<SpriteAnimationFrameEvent> {
if(_events == null) return [];
if(_events.length == 0) return [];
var _resulting_events = [];
for(_event in _events) {
if(_event.frame == _frame) {
_resulting_events.push(_event);
}
}
return _resulting_events;
} //parse_event_for_frame
function parse_source_size_for_frame( _sources:Array<SpriteAnimationFrameSource>, _frame:Int ) : Vector {
if(_sources != null) {
for(_source in _sources) {
if(_source.frame == _frame) {
return _source.size;
} //matching frame
} //each source
} //sources != null
return frame_size;
} //parse_source_size_for_frame
function parse_source_pos_for_frame( _sources:Array<SpriteAnimationFrameSource>, _frame:Int ) : Vector {
if(_sources != null) {
for(_source in _sources) {
if(_source.frame == _frame) {
return _source.pos;
} //matching frame
} //each source
} //sources != null
return new Vector();
} //parse_source_pos_for_frame
function parse_source_for_frame( _sources:Array<SpriteAnimationFrameSource>, _frame:Int ) : Rectangle {
var _explicit_source : Rectangle = null;
if(_sources != null) {
for(_source in _sources) {
if(_source.frame == _frame) {
_explicit_source = _source.source;
} //matching frame
} //each source
} //sources != null
//here is where we try the best guess for the frame given
//unless an explicit frame source was given
if(_explicit_source == null) {
var result = new Rectangle(0, 0, frame_size.x, frame_size.y);
if(sprite.texture != null) {
switch(type) {
case SpriteAnimationType.animated_uv: {
var frames_per_row = ( sprite.texture.width - (sprite.texture.width % frame_size.x) ) / frame_size.x;
var image_row = Math.ceil( _frame / frames_per_row );
var image_x = ((_frame-1) * frame_size.x) % sprite.texture.width;
var image_y = ((image_row-1) * frame_size.y);
result = new Rectangle( image_x, image_y, frame_size.x, frame_size.y );
} //animated_uv
default : {}
} //type
} //texture != null
return result;
} else {
return _explicit_source;
} //if explicit source
} //parse_source_for_frame
function parse_frame_sources_set( _sources:Array<Dynamic> ) : Array<SpriteAnimationFrameSource> {
if(_sources == null) return [];
var resulting_sources = [];
for(_json_source in _sources) {
var _json_size : Dynamic = _json_source.size;
var _json_source_rect : Dynamic = _json_source.source;
var _json_pos : Dynamic = _json_source.pos;
var _x : Float = 0;
var _y : Float = 0;
var _w : Float = 0;
var _h : Float = 0;
var _sx : Float = 0;
var _sy : Float = 0;
var _px : Float = 0;
var _py : Float = 0;
if(_json_source_rect != null) {
_x = Std.parseFloat(_json_source_rect.x);
_y = Std.parseFloat(_json_source_rect.y);
_w = Std.parseFloat(_json_source_rect.w);
_h = Std.parseFloat(_json_source_rect.h);
}
if(_json_size != null) {
_sx = Std.parseFloat(_json_size.x);
_sy = Std.parseFloat(_json_size.y);
}
if(_json_pos != null) {
_px = Std.parseFloat(_json_pos.x);
_py = Std.parseFloat(_json_pos.y);
}
var _source : SpriteAnimationFrameSource = {
frame : Std.parseInt(_json_source.frame),
source : new Rectangle(_x, _y, _w, _h),
size : new Vector( _sx, _sy ),
pos : new Vector( _px, _py )
}
resulting_sources.push(_source);
} //each size in the list
return resulting_sources;
} //parse_frame_sources_set
function parse_event_set( _events:Array<Dynamic> ) : Array<SpriteAnimationFrameEvent> {
if(_events == null) return [];
var resulting_events = [];
for(_json_event in _events) {
//frames can have empty events, in which case it will assume the animationname.event.frame
if(_json_event.frame != null) {
var _event : SpriteAnimationFrameEvent = {
frame : Std.parseInt(_json_event.frame),
event : (_json_event.event == null) ? "" : (_json_event.event)
}
resulting_events.push(_event);
} //frame != null
} //each event in json events
return resulting_events;
} //parse_event_set
function parse_frameset_range( _frameset:Array<Int>, regex:EReg, _frame:String ) : Void {
var _start : Int = Std.parseInt( regex.matched(1) );
var _end : Int = Std.parseInt( regex.matched(3) );
var _count : Int = Std.int(Math.abs( _start - _end ));
//If they are the same, that's a silly range but allow it
if(_count == 0) {
_frameset.push( _start );
} else {
//if reversed, count backward from the end instead
if(_start > _end) {
for( _i in 0 ... _count+1 ) {
_frameset.push( _start - _i );
}
} else {
for( i in _start ... _end+1 ) {
_frameset.push( i );
}
} //_start < _end
} //_count == 0
} //parse_frameset_range
function parse_frameset_hold( _frameset:Array<Int>, regex:EReg, _frame:String ) : Void {
var _frame_index : Int = Std.parseInt( regex.matched(1) );
var _amount : Int = Std.parseInt( regex.matched(3) );
for( _i in 0 ... _amount ) {
_frameset.push( _frame_index );
}
} //parse_frameset_range
function parse_frameset_prev_hold( _frameset:Array<Int>, regex:EReg, _frame:String ) : Void {
assert(_frameset.length > 0, 'Animation frames given a hold with no prior frame, if you want to do that you can use `1 hold 10` where 1 is the frame index, 10 is the amount.');
var _frame : Int = _frameset[ _frameset.length - 1 ];
var _amount : Int = Std.parseInt( regex.matched(2) );
for( _i in 0 ... _amount ) {
_frameset.push( _frame );
}
} //parse_frameset_prev_hold
function parse_frameset_frame( _frameset:Array<Int>, regex:EReg, _frame:String ) : Void {
var _frame : Int = Std.parseInt( regex.matched(1) );
_frameset.push( _frame );
} //parse_frameset_frame
function parse_frameset( _json_frameset:Array<String> ) : Array<Int> {
var _final_frameset = [];
for(_frame in _json_frameset) {
//match a range (frame)-(frame)
if( frame_range_regex.match( _frame ) ) {
parse_frameset_range( _final_frameset, frame_range_regex, _frame );
} else
//match the (frame) hold (amount)
if( frame_hold_regex.match( _frame ) ) {
parse_frameset_hold( _final_frameset, frame_hold_regex, _frame );
} else
//match the hold (amount) from previous frame
if( frame_hold_prev_regex.match( _frame ) ) {
parse_frameset_prev_hold( _final_frameset, frame_hold_prev_regex, _frame );
} else
//match the single value frames
if( frame_regex.match( _frame ) ) {
parse_frameset_frame( _final_frameset, frame_regex, _frame );
}
} //for each frame
return _final_frameset;
}
} //SpriteAnimationData
import luxe.Log.*;
import luxe.Visual;
import luxe.Vector;
import luxe.Rectangle;
import luxe.Resources;
import phoenix.geometry.Geometry;
import phoenix.geometry.QuadGeometry;
class SpriteRenderer extends VisualRenderer {
@:isVar public var centered (default, set) : Bool = true;
@:isVar public var flipx (default, set) : Bool = false;
@:isVar public var flipy (default, set) : Bool = false;
@:isVar public var uv (default, set) : Rectangle;
public var geometry_quad : QuadGeometry;
public function new( _options:SpriteOptions ) {
assertnull(_options, 'Sprite requires non-null options');
def(_options.name, 'SpriteRenderer');
super(_options);
} // new
override function onadded(){
uv = new Rectangle();
//centered
if(options.centered != null) {
centered = options.centered;
}
//flipx
if(options.flipx != null) {
flipx = options.flipx;
}
//flipy
if(options.flipy != null) {
flipy = options.flipy;
}
//create visual
super.onadded();
}
override function on_geometry_created() {
super.on_geometry_created();
if(texture != null) {
//because the default is 0,0,1,1 uv for the quad, we don't want that when
//textures are padded (like on web)
uv = def(options.uv, new Rectangle(0,0,texture.width,texture.height));
//if texture is render target, flipy
if(texture.resource_type == ResourceType.render_texture) {
flipy = true;
}
} //texture !null
//set the origin and centered once created
centered = !!centered;
//and re assign the flip values
flipx = !!flipx;
flipy = !!flipy;
} //on_geometry_created
override function set_geometry( _g:Geometry ) {
geometry_quad = cast _g;
return super.set_geometry(_g);
} //set_geometry
override public function ondestroy() {
uv = null;
geometry_quad = null;
super.ondestroy();
}
//Helper functions
/** Returns true if a point is inside the sprite, takes into account the sprite transform,
which includes more cost than simple AABB like `point_inside_AABB` */
public function point_inside( _p:Vector ) : Bool {
if(geometry == null) return false;
return Luxe.utils.geometry.point_in_geometry(_p, geometry);
} //point_inside
/** Returns true if a point is inside the AABB unrotated */
public function point_inside_AABB(_p:Vector) : Bool {
if(pos == null) return false;
if(size == null) return false;
//scaled size
var _s_x = size.x * scale.x;
var _s_y = size.y * scale.y;
if(centered) {
var _hx = _s_x / 2;
var _hy = _s_y / 2;
if(_p.x < pos.x - _hx) return false;
if(_p.y < pos.y - _hy) return false;
if(_p.x > pos.x+_s_x - _hx) return false;
if(_p.y > pos.y+_s_y - _hy) return false;
} else {
if(_p.x < pos.x) return false;
if(_p.y < pos.y) return false;
if(_p.x > pos.x+_s_x) return false;
if(_p.y > pos.y+_s_y) return false;
}
return true;
} //point_inside_AABB
//Properties
//UV / source rect
function set_uv(_uv:Rectangle) : Rectangle {
if(_uv == null) return uv = _uv;
if(geometry_quad != null) {
geometry_quad.uv(_uv);
}
uv = _uv;
Rectangle.listen( uv, _uv_change );
return uv;
}
//Flipping
function set_flipy(_v:Bool) {
if(_v == flipy) {
return flipy;
}
if(geometry_quad != null) {
geometry_quad.flipy = _v;
}
return flipy = _v;
} //set_flipy
function set_flipx(_v:Bool) {
if(_v == flipx) {
return flipx;
}
if(geometry_quad != null) {
geometry_quad.flipx = _v;
}
return flipx = _v;
} //set_flipv
//Size
override function set_size( _v:Vector ) : Vector {
//resize the mesh vertices themselves, as scale is relative to this size
//if explicitly set
if(geometry_quad != null) {
geometry_quad.resize( new Vector( _v.x, _v.y ) );
//If the user doesn't specify a custom origin, we try and work with the center
if(!_has_custom_origin) {
if(centered) {
//half of the new size
origin = _v.clone().divideScalar(2);
}
}
}
//done
return super.set_size(_v);
} //set_size
//Centered
function set_centered(_c:Bool) : Bool {
//centered geometry affects the origin directly
if(size != null) {
if(_c) {
origin = new Vector(size.x/2, size.y/2);
} else {
origin = new Vector();
}
} //size != null
return centered = _c;
} //set_centered
//An internal callback for when x y or w or h on a transform changes
function _uv_change(_v:Float) { this.set_uv(uv); }
} //SpriteRenderer
typedef SpriteOptions = {
> VisualRenderer.VisualOptions,
/** if set, the sprite will be centered */
@:optional var centered : Bool;
/** if set, the sprite will be flipped horizontally */
@:optional var flipx : Bool;
/** if set, the sprite will be flipped vertically */
@:optional var flipy : Bool;
/** specify the uv rectangle inside the texture, in texture space (pixels) */
@:optional var uv : Rectangle;
} //SpriteOptions
import luxe.Color;
import luxe.Component;
import luxe.Entity;
import luxe.Vector;
import luxe.Rectangle;
import luxe.utils.Maths;
import luxe.resource.Resource;
import phoenix.geometry.Geometry;
import phoenix.geometry.QuadGeometry;
import phoenix.Shader;
import phoenix.Batcher;
import phoenix.Quaternion;
import phoenix.Texture;
import phoenix.Transform;
import luxe.options.RenderProperties;
import luxe.options.ComponentOptions;
import luxe.Log.*;
class VisualRenderer extends Component {
/** the size of this geometry (only makes sense for QuadGeometry) */
@:isVar public var size (default,set) : Vector;
/** the geometry this visual contains */
@:isVar public var geometry (default,set) : Geometry;
/** the geometry static flag. This is a hint on how to render the geometry for performance */
@:isVar public var locked (default,set) : Bool = false;
/** the texture for the geometry */
@:isVar public var texture (default,set) : Texture;
/** the shader for the geometry */
@:isVar public var shader (default,set) : Shader;
/** the base color */
@:isVar public var color (default,set) : Color;
/** the visibility */
@:isVar public var visible (default,set) : Bool = true;
/** the geometry depth value (see guides)*/
@:isVar public var depth (default,set) : Float = 0.0;
/** If note null, the geometry will be clipped to this rectangle region (in world space). */
@:isVar public var clip_rect (default,set) : Rectangle;
/** convenience: controls the rotation around the z axis, in radians. */
@:isVar public var radians (get,set) : Float = 0.0;
/** convenience: controls the rotation around the z axis, in degrees. */
public var rotation_z (get,set) : Float;
//private
var _rotation_euler : Vector;
var _rotation_quat : Quaternion;
var _has_custom_origin : Bool = false;
public var options : Dynamic;
public function new( _options:VisualOptions ) {
options = _options;
assertnull(_options, 'Visual requires non-null options');
def(options.name, 'VisualRenderer');
super({ name:options.name });
} // new
override function onadded(){
//cached values
//these need to be before super
_rotation_euler = new Vector();
_rotation_quat = new Quaternion();
//create the position value so we can exploit it a bit
color = new Color();
size = new Vector();
//texture
if(options.texture != null) {
texture = options.texture;
}
//shader
if(options.shader != null) {
shader = options.shader;
}
//color
if(options.color != null) {
color = options.color;
}
//depth
if(options.depth != null) {
depth = options.depth;
}
//visible
if(options.visible != null) {
visible = options.visible;
}
//size is interesting, as it's possibly based on texture
//user specified a size
if(options.size != null) {
size = options.size;
//the size is explicit, so make the geometry
_create_geometry();
} else {
//if the texture isn't invalid entirely
if(texture != null) {
size = new Vector(texture.width, texture.height);
_create_geometry();
} else {
//default to a value big enough to see
size = new Vector(64,64);
_create_geometry();
} //texture !=null
} //
}
var _creating_geometry : Bool = false;
@:noCompletion public function _create_geometry() {
//if they give a geometry, don't create one
if(options.geometry == null) {
if(options.no_geometry == null || options.no_geometry == false) {
_creating_geometry = true;
var _batcher : Batcher = null;
if(options.no_batcher_add == null || options.no_batcher_add == false) {
if(options.batcher != null) {
_batcher = options.batcher;
} else {
_batcher = Luxe.renderer.batcher;
}
}
geometry = new QuadGeometry({
id:name + '.visual',
x:0,
y:0,
w:size.x,
h:size.y,
scale: new Vector(1,1,1),
texture : texture,
color : color,
shader : shader,
batcher : _batcher,
depth : (options.depth == null) ? 0 : options.depth,
visible : (options.visible == null) ? visible : options.visible
});
_creating_geometry = false;
//call the geometry create listener
on_geometry_created();
} //no_geometry is not present
} else {
geometry = options.geometry;
}
//default to the visual name
if(geometry != null) {
geometry.id = name + ".visual";
geometry.transform.id = name + ".visual.transform";
}
//custom provided origin will override any until now
if(options.origin != null) {
_has_custom_origin = true;
origin = options.origin;
}
//apply the rotation if any
if(options.rotation_z != null) {
rotation_z = options.rotation_z;
}
} //create_geometry
override public function ondestroy() {
//drop the geometry
if(geometry != null && geometry.added ) {
geometry.drop( true );
}
//clear our references to these
transform = null;
options = null;
geometry = null;
texture = null;
shader = null;
color = null;
size = null;
clip_rect = null;
_rotation_euler = null;
_rotation_quat = null;
} //ondestroy
function on_geometry_created() {
// geometry.transform.parent = transform;
} //on_geometry_created
//Visibility properties
function set_visible(_v:Bool) {
visible = _v;
//careful
if(geometry != null) {
geometry.visible = visible;
}
return visible;
} //set_visible
function set_depth(_v:Float) {
if(geometry != null) {
geometry.depth = _v;
} //geometry
return depth = _v;
} //set_depth
//Color properties
function set_color(_c:Color) {
if(color != null && geometry != null) {
geometry.color = _c;
}
return color = _c;
} //set_color
function set_texture(_t:Texture) {
if(geometry != null && geometry.texture != _t) {
geometry.texture = _t;
} //geometry!=null
return texture = _t;
}
function set_shader(_s:Shader) {
if(geometry != null && geometry.shader != _s) {
geometry.shader = _s;
} //geometry!=null
return shader = _s;
}
//Geometry
var ignore_texture_on_geometry_change : Bool = false;
function set_geometry(_g:Geometry) : Geometry {
//same geometry?
if(geometry == _g) {
return geometry;
}
//kill the existing geometry first
if(geometry != null) {
geometry.drop();
}
//store the new one
geometry = _g;
//rebind it's colors and whatever else
if(geometry != null) {
//make sure it's attached
geometry.transform.parent = entity.transform;
_verbose(' assign geometry transform as child : $geometry.id to $name');
//:todo: This block is dumb
//and was solving some obscure issue
//and needs to be redone as it
//causes more issues than it solves.
{
if(_creating_geometry == false) {
geometry.color = color;
geometry.depth = depth;
geometry.visible = visible;
// geometry.shader = shader;
if(!ignore_texture_on_geometry_change) {
// geometry.texture = texture;
}
} //_creating_geometry == false
}
} //geometry != null
return geometry;
} //set_geometry
//Transforms
override function entity_parent_change( _parent:Transform ) {
if(geometry != null) {
//make sure it's attached
geometry.transform.parent = entity.transform;
}
} //entity_parent_change
override function entity_rotation_change( _rotation:Quaternion ) {
//update caches
_rotation_euler.setEulerFromQuaternion(_rotation);
_rotation_quat.copy(_rotation);
} //entity_rotation_change
//Size
function set_size( _v:Vector ) : Vector {
size = _v;
if(size != null)
Vector.Listen( size, _size_change );
return size;
} //set_size
//Rotation
function get_rotation_z() : Float {
return Maths.degrees(radians);
} //get_rotation
function set_rotation_z( _degrees:Float ) : Float {
radians = Maths.radians(_degrees);
return _degrees;
} //set_rotation_z
function set_radians(_r:Float) : Float {
_rotation_euler.z = _r;
_rotation_quat.setFromEuler( _rotation_euler );
rotation = _rotation_quat.clone();
return radians = _r;
} //set_radians
function get_radians() : Float {
return radians;
} //get_radians
//Locked
function set_locked(_l:Bool) : Bool {
if(geometry != null) {
geometry.locked = _l;
}
return locked = _l;
} //set_locked
//Geometry properties
//Clip rect
function set_clip_rect(_val : Rectangle) : Rectangle {
if(geometry != null) {
geometry.clip_rect = _val;
}
return clip_rect = _val;
}
//Size
//An internal callback for when x y or z on a size changes
function _size_change( _v:Float ) { this.set_size( size ); }
} //VisualRenderer
typedef VisualOptions = {
> RenderProperties,
> ComponentOptions,
/** the size of the geometry to create */
@:optional var size : Vector;
/** the base color for the geometry */
@:optional var color : Color;
/** the texture for the geometry */
@:optional var texture : Texture;
/** the shader for the geometry */
@:optional var shader : Shader;
/** the rotation around the z access, in degrees. convenience. */
@:optional var rotation_z : Float;
/** if specified, this geometry will be used instead. */
@:optional var geometry : Geometry;
/** if specified, no geometry will be created. */
@:optional var no_geometry : Bool;
/** if specified, the geometry will not be added to any batcher. */
@:optional var no_batcher_add : Bool;
/** the transform origin */
@:optional var origin : Vector;
} //VisualOptions
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment