/*
* cocos2d for iPhone: http://www.cocos2d-iphone.org
*
* Copyright (c) 2008, 2009 Jason Booth
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/*
* A ribbon is a dynamically generated list of polygons drawn as a single or series
* of triangle strips. The primary use of Ribbon is as the drawing class of Motion Streak,
* but it is quite useful on it's own. When manually drawing a ribbon, you can call addPointAt
* and pass in the parameters for the next location in the ribbon. The system will automatically
* generate new polygons, texture them accourding to your texture width, etc, etc.
*
* Ribbon data is stored in a RibbonSegment class. This class statically allocates enough verticies and
* texture coordinates for 50 locations (100 verts or 48 triangles). The ribbon class will allocate
* new segments when they are needed, and reuse old ones if available. The idea is to avoid constantly
* allocating new memory and prefer a more static method. However, since there is no way to determine
* the maximum size of some ribbons (motion streaks), a truely static allocation is not possible.
*
*/
#import "CCRibbon.h"
#import "CCTextureCache.h"
#import "Support/CGPointExtension.h"
#import "ccMacros.h"
//
// Ribbon
//
@implementation CCRibbon
@synthesize blendFunc=blendFunc_;
@synthesize color=color_;
@synthesize textureLength = textureLength_;
+(id)ribbonWithWidth:(float)w image:(NSString*)path length:(float)l color:(ccColor4B)color fade:(float)fade
{
self = [[[self alloc] initWithWidth:w image:path length:l color:color fade:fade] autorelease];
return self;
}
-(id)initWithWidth:(float)w image:(NSString*)path length:(float)l color:(ccColor4B)color fade:(float)fade
{
self = [super init];
if (self)
{
segments_ = [[NSMutableArray alloc] init];
deletedSegments_ = [[NSMutableArray alloc] init];
/* 1 initial segment */
CCRibbonSegment* seg = [[CCRibbonSegment alloc] init];
[segments_ addObject:seg];
[seg release];
textureLength_ = l;
color_ = color;
fadeTime_ = fade;
lastLocation_ = CGPointZero;
lastWidth_ = w/2;
texVPos_ = 0.0f;
curTime_ = 0;
pastFirstPoint_ = NO;
/* XXX:
Ribbon, by default uses this blend function, which might not be correct
if you are using premultiplied alpha images,
but 99% you might want to use this blending function regarding of the texture
*/
blendFunc_.src = GL_SRC_ALPHA;
blendFunc_.dst = GL_ONE_MINUS_SRC_ALPHA;
self.texture = [[CCTextureCache sharedTextureCache] addImage:path];
/* default texture parameter */
ccTexParams params = { GL_LINEAR, GL_LINEAR, GL_REPEAT, GL_REPEAT };
[texture_ setTexParameters:¶ms];
}
return self;
}
-(void)dealloc
{
[segments_ release];
[deletedSegments_ release];
[texture_ release];
[super dealloc];
}
// rotates a point around 0, 0
-(CGPoint)rotatePoint:(CGPoint)vec rotation:(float)a
{
float xtemp = (vec.x * cosf(a)) - (vec.y * sinf(a));
vec.y = (vec.x * sinf(a)) + (vec.y * cosf(a));
vec.x = xtemp;
return vec;
}
-(void)update:(ccTime)delta
{
curTime_+= delta;
delta_ = delta;
}
-(float)sideOfLine:(CGPoint)p l1:(CGPoint)l1 l2:(CGPoint)l2
{
CGPoint vp = ccpPerp(ccpSub(l1, l2));
CGPoint vx = ccpSub(p, l1);
return ccpDot(vx, vp);
}
// adds a new segment to the ribbon
-(void)addPointAt:(CGPoint)location width:(float)w
{
location.x *= CC_CONTENT_SCALE_FACTOR();
location.y *= CC_CONTENT_SCALE_FACTOR();
w = w*0.5f;
// if this is the first point added, cache it and return
if (!pastFirstPoint_)
{
lastWidth_ = w;
lastLocation_ = location;
pastFirstPoint_ = YES;
return;
}
CGPoint sub = ccpSub(lastLocation_, location);
float r = ccpToAngle(sub) + (float)M_PI_2;
CGPoint p1 = ccpAdd([self rotatePoint:ccp(-w, 0) rotation:r], location);
CGPoint p2 = ccpAdd([self rotatePoint:ccp(w, 0) rotation:r], location);
float len = sqrtf(powf(lastLocation_.x - location.x, 2) + powf(lastLocation_.y - location.y, 2));
float tend = texVPos_ + len/textureLength_;
CCRibbonSegment* seg;
// grab last segment
seg = [segments_ lastObject];
// lets kill old segments
for (CCRibbonSegment* seg2 in segments_)
{
if (seg2 != seg && seg2->finished)
{
[deletedSegments_ addObject:seg2];
}
}
[segments_ removeObjectsInArray:deletedSegments_];
// is the segment full?
if (seg->end >= 50)
[segments_ removeObjectsInArray:deletedSegments_];
// grab last segment and append to it if it's not full
seg = [segments_ lastObject];
// is the segment full?
if (seg->end >= 50)
{
CCRibbonSegment* newSeg;
// grab it from the cache if we can
if ([deletedSegments_ count] > 0)
{
newSeg = [deletedSegments_ objectAtIndex:0];
[newSeg retain]; // will be released later
[deletedSegments_ removeObject:newSeg];
[newSeg reset];
}
else
{
newSeg = [[CCRibbonSegment alloc] init]; // will be released later
}
newSeg->creationTime[0] = seg->creationTime[seg->end - 1];
int v = (seg->end-1)*6;
int c = (seg->end-1)*4;
newSeg->verts[0] = seg->verts[v];
newSeg->verts[1] = seg->verts[v+1];
newSeg->verts[2] = seg->verts[v+2];
newSeg->verts[3] = seg->verts[v+3];
newSeg->verts[4] = seg->verts[v+4];
newSeg->verts[5] = seg->verts[v+5];
newSeg->coords[0] = seg->coords[c];
newSeg->coords[1] = seg->coords[c+1];
newSeg->coords[2] = seg->coords[c+2];
newSeg->coords[3] = seg->coords[c+3];
newSeg->end++;
seg = newSeg;
[segments_ addObject:seg];
[newSeg release]; // it was retained before
}
if (seg->end == 0)
{
// first edge has to get rotation from the first real polygon
CGPoint lp1 = ccpAdd([self rotatePoint:ccp(-lastWidth_, 0) rotation:r], lastLocation_);
CGPoint lp2 = ccpAdd([self rotatePoint:ccp(+lastWidth_, 0) rotation:r], lastLocation_);
seg->creationTime[0] = curTime_ - delta_;
seg->verts[0] = lp1.x;
seg->verts[1] = lp1.y;
seg->verts[2] = 0.0f;
seg->verts[3] = lp2.x;
seg->verts[4] = lp2.y;
seg->verts[5] = 0.0f;
seg->coords[0] = 0.0f;
seg->coords[1] = texVPos_;
seg->coords[2] = 1.0f;
seg->coords[3] = texVPos_;
seg->end++;
}
int v = seg->end*6;
int c = seg->end*4;
// add new vertex
seg->creationTime[seg->end] = curTime_;
seg->verts[v] = p1.x;
seg->verts[v+1] = p1.y;
seg->verts[v+2] = 0.0f;
seg->verts[v+3] = p2.x;
seg->verts[v+4] = p2.y;
seg->verts[v+5] = 0.0f;
seg->coords[c] = 0.0f;
seg->coords[c+1] = tend;
seg->coords[c+2] = 1.0f;
seg->coords[c+3] = tend;
texVPos_ = tend;
lastLocation_ = location;
lastPoint1_ = p1;
lastPoint2_ = p2;
lastWidth_ = w;
seg->end++;
}
-(void) draw
{
[super draw];
if ([segments_ count] > 0)
{
// Default GL states: GL_TEXTURE_2D, GL_VERTEX_ARRAY, GL_COLOR_ARRAY, GL_TEXTURE_COORD_ARRAY
// Needed states: GL_TEXTURE_2D, GL_VERTEX_ARRAY, GL_TEXTURE_COORD_ARRAY
// Unneeded states: GL_COLOR_ARRAY
glDisableClientState(GL_COLOR_ARRAY);
glBindTexture(GL_TEXTURE_2D, [texture_ name]);
BOOL newBlend = blendFunc_.src != CC_BLEND_SRC || blendFunc_.dst != CC_BLEND_DST;
if( newBlend )
glBlendFunc( blendFunc_.src, blendFunc_.dst );
for (CCRibbonSegment* seg in segments_)
[seg draw:curTime_ fadeTime:fadeTime_ color:color_];
if( newBlend )
glBlendFunc(CC_BLEND_SRC, CC_BLEND_DST);
// restore default GL state
glEnableClientState( GL_COLOR_ARRAY );
}
}
#pragma mark Ribbon - CocosNodeTexture protocol
-(void) setTexture:(CCTexture2D*) texture
{
[texture_ release];
texture_ = [texture retain];
[self setContentSizeInPixels: texture.contentSizeInPixels];
/* XXX Don't update blending function in Ribbons */
}
-(CCTexture2D*) texture
{
return texture_;
}
@end
#pragma mark -
#pragma mark RibbonSegment
@implementation CCRibbonSegment
-(id)init
{
self = [super init];
if (self)
{
[self reset];
}
return self;
}
- (NSString*) description
{
return [NSString stringWithFormat:@"<%@ = %08X | end = %i, begin = %i>", [self class], self, end, begin];
}
- (void) dealloc
{
CCLOGINFO(@"cocos2d: deallocing %@", self);
[super dealloc];
}
-(void)reset
{
end = 0;
begin = 0;
finished = NO;
}
-(void)draw:(float)curTime fadeTime:(float)fadeTime color:(ccColor4B)color
{
GLubyte r = color.r;
GLubyte g = color.g;
GLubyte b = color.b;
GLubyte a = color.a;
if (begin < 50)
{
// the motion streak class will call update and cause time to change, thus, if curTime_ != 0
// we have to generate alpha for the ribbon each frame.
if (curTime == 0)
{
// no alpha over time, so just set the color
glColor4ub(r,g,b,a);
}
else
{
// generate alpha/color for each point
glEnableClientState(GL_COLOR_ARRAY);
uint i = begin;
for (; i < end; ++i)
{
int idx = i*8;
colors[idx] = r;
colors[idx+1] = g;
colors[idx+2] = b;
colors[idx+4] = r;
colors[idx+5] = g;
colors[idx+6] = b;
float alive = ((curTime - creationTime[i]) / fadeTime);
if (alive > 1)
{
begin++;
colors[idx+3] = 0;
colors[idx+7] = 0;
}
else
{
colors[idx+3] = (GLubyte)(255.f - (alive * 255.f));
colors[idx+7] = colors[idx+3];
}
}
glColorPointer(4, GL_UNSIGNED_BYTE, 0, &colors[begin*8]);
}
glVertexPointer(3, GL_FLOAT, 0, &verts[begin*6]);
glTexCoordPointer(2, GL_FLOAT, 0, &coords[begin*4]);
glDrawArrays(GL_TRIANGLE_STRIP, 0, (end - begin) * 2);
}
else
finished = YES;
}
@end