From cf11351ffe926c93a412955f03c32490a039558d Mon Sep 17 00:00:00 2001 From: Daniel Esteban Date: Sun, 8 Feb 2026 17:39:20 +0100 Subject: [PATCH] first commit --- conejo.fire-pixel.zip | Bin 0 -> 4072 bytes data.go | 1012 +++++ firefly.toml | 5 + go.mod | 9 + go.sum | 4 + main.go | 166 + pixeljs-master/converter.php | 56 + pixeljs-master/img/ninja.js | 3 + pixeljs-master/img/ninja.png | Bin 0 -> 4160 bytes pixeljs-master/index.php | 155 + pixeljs-master/kinetic-v3.10.5.js | 6600 +++++++++++++++++++++++++++++ 11 files changed, 8010 insertions(+) create mode 100644 conejo.fire-pixel.zip create mode 100644 data.go create mode 100644 firefly.toml create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 pixeljs-master/converter.php create mode 100644 pixeljs-master/img/ninja.js create mode 100644 pixeljs-master/img/ninja.png create mode 100644 pixeljs-master/index.php create mode 100644 pixeljs-master/kinetic-v3.10.5.js diff --git a/conejo.fire-pixel.zip b/conejo.fire-pixel.zip new file mode 100644 index 0000000000000000000000000000000000000000..511a3991ea81e7b48cf1933b44cbc15a78a6386d GIT binary patch literal 4072 zcmZ{nXHZk?7KRf_KtdIeUPPKy5rPzhpkSm)6NS(O48;(N5Na++QBZo9VnB)nL69KQ zf)u4FLJmg+0)`F|LJw6DZan_DbDX)~%)8dw`}^K^@0q>-JeH=6Ov(TNz=^KYfUfy< z9trv+Oc#hQm%t#`pg{Qs)t>;n1_0nb0IE#;e^*a&>1SlJQV^%XLr#_kHw0k=zc+u@pjC(Wt-0X5(@7sJryzalVa$6AY z44eP}N&o<0qUX7|dHep38t?%S-pj@)521Q{+hCkb1IB z?~poxl2S?$l4F1owLIzNl}&UhCBHs+dqqb;D_qk;97en##|R^8deY0nBXQ{GLTC!s zr6RSI60sXJ97rXw#X1#6?8289iejc<>*A}$&TC6J<)hCMtK?Gg;eKerc*%}RQHW!1 z2z+n2GXCk1RV}uCPNUe}s&+0U>gyd~L^r2y;M?#61XC_32>w?APJe;RcKJZV#jK5a zFM(wzB&u;67;&O(zP^g-GH)wJ#UfuY?JOIIVpG5A8bXl!M4w?C-#mrFVl4ALK>zu1 zlf+;JNBUgX0u3R_JCxe0A*(IZs0F*mN=8f%D#JO!6xx@~3Z{7fdK<7w1hNbc7WNDR5r*UfgZO-p@^x|#0U2d3yrnPB4fS;Y=Eqk*8- zG%nRXSV9jWwKhKf4k*9iM~Y~4CnB&&B!O?~^z50FhK8&wh ziPNVc+pL=(!D46c-ASOI%_#UGDIxLfv~e8k&OV<|Oy(U{9-*{t*6!Q1?`Na-_7ub3 z&fnTnYBp6Nu45@7QMr_MVeD~adr=~#y`M(=I<|I0f2W~Q z%xrjx^HGc#lU9t#Ec=|~Rlxuz-}orUS`7sTZH@$uSdd{1$CrLcQXK6)$C)-S)v#@o zQu*P2S^DE-ck(Hw;xbc(H5srEdxuN+jZgs}G%E%k*=)%-%RUSOOMw|gt+^SKv>1Bl z?-8#m3k4H}++HL)c=8IYH)(jGt5o2M!45xDGC+JbweBYX%F^thR;`z`9(XlvxW|(P ztJC2*VI!t@9J(A!B)*M!K6%kGh%Y4&oW{_TUjhca-l7NqR$l&J)4aiK)orf zYzKu6E7d`fv6*6KGjIo;yn)C}8aQq5zA)4WZZybv@6GZGavHJ3CzuvIyKK zbbc1WEKXMfF>BM_K+Nv+Ss>@MBx{2@!X<@*Y%N!1&T*ikw2>TL%v+ zG%%;Ow!0~#A=@cN(08{5deOTz%d?ltXC|SSt&vX_Kb_5c=s&ddR z^YYe0e-rNjDmh7-}sYqNT+@%ukW>~3CydgmKq>+U(0s(Wiz>F{@|(UQzF zTeMjBt%=e-H8b1=LctW7u(lX@A*wZOVqnxaGMILr)=qlaoNuV^#Yy&=ikdtoN0E=e z9#2F~9FHH9qDWCR@=@Q^K5x_v*J1q8E;_=waS9#b)JR1CaA<5q|A03Zp(E@YFSPz_ zU47%^;3nb|--2?fr$Bx<)sI4cIM%mAA{^>#AwTTvb0HD+q;`@oDO(0_5R4=3LbeE3 zBS-tF!Uv9LqLA~bWX#L-BFGm0>ZH&7RG+U1NeO>Tnn%cx6N3LwTjVGe-KIB6^=q3P zp(5KNN2rLl$(5b;h6(3_)rR!8-IkN1(>lY*ALF4iD2$~@J8}+q4NA!m{1ht-dAA)6k&$Fji_?>!2}`eb*<-)m~tB^sWtiyz(DXcj*jc{dTh z59hi>ok2heq<8J{!aZP-D+wQAm=!zRx!Pq--&S>aL$tma2E((0#0qlpIK`I|s zKgey^@Bkm53ZCfla2V@5wL9LW`Q}xkvX2`$LY^sjgTNU%+DX;fcVvm8PNT#yFJp_) z8+gvimFcNYU)?bu77B_ikuz* z9dWF@i5}2=mCmu!5RLM=Eg&-XUZsU(6eWP3Fd7K9VzDsK$L2poW-3&lzqy(%aNfPz z_I7@k8Pwo`Q9>oZagJ(`l~vG)oMwt~ZOMxm1#p^awzQ3fzKsP`)yl9sN4mP}+LBv2 zNM2qWo3GJ*Qw~ydElfX2#rj6HJ^X23in0CkP|L-~Z0vYeIa#um@sdfFxcdv7rK?bv z_X)O-KxHA7o35|^is{W(mfn|QjuUh$3%x7#IAgXWN3PgCgWK(`DGi&2+Du-g*N#>0#1$9n$IR8bih#X3r*^;o{Z`o_Aod~y7!0mr&r`9wVr3l}R97OMCw->h%rD@3=Xsep zMa4=53()Nbrh2?i-lvbtO6sUZGr57nB}HrTvDx|0&S58F6DNj+vk=cSjjQkEoxf>m z>2ckp;$C-USxVE5yqPCK73HSrx=h6LI{Le*xwYW7-uGB7(ZA0NTul?6%{@u5N8y|) zGV{sjzei-)iZ)X>YkirZ|5&kp2remn0NF)_S2; z2ec&wn3ZG%RjLeEsoC`Rp{R$XS=n?@gTiTt7j!v*~Z z%bcdj@RHJye&KNK&ARhMo^G(`SB`e%Oo-U< zm!RhH#m2@%xSFAry3bJfzlXZ#=N}=k7SFw(X`m(r3U(RE;xF3TCrr6W;#H${R^P!cSq8+(dzy`7)W)kjG};z3nYqh7+$ z>QJi3oPJPP-;tYn%H^$>0>cR_UD_K@TA8Nt8DhGiR|ga7v@Ygr_|IVAI(~CwOGdcd%w-^ zp0B>agN z1|nUy9SeWbbzzvHYvpAeWbT&m<=AryC!)by&m{d$siKlz^IxFK2<{vCDwHLULmopL zXA0clm$@ed(%w7xr+qht{H3HS&t~)8h8)hVT?QAp;+j?V-W;DT>5=9G*CVi6pNEcE zzCKM}Gq>l3JYN|}SCneY@-Q;?nY88T8C&0d4~=O@R!`jQ^j5>7e+} zT8;kIepB(Eo;EjXzbtf`wKQe^ Sh3<5^WdeMncM#XFtN#Mp_HHZy literal 0 HcmV?d00001 diff --git a/data.go b/data.go new file mode 100644 index 0000000..81992c6 --- /dev/null +++ b/data.go @@ -0,0 +1,1012 @@ +package main + +// Auto-generated from ninja.js +const numPixels = 501 + +var imgCoord = [501][2]int16{ + {-50, -120}, + {-40, -120}, + {-30, -120}, + {-20, -120}, + {-10, -120}, + {0, -120}, + {10, -120}, + {-70, -110}, + {-60, -110}, + {-50, -110}, + {-40, -110}, + {-30, -110}, + {-20, -110}, + {-10, -110}, + {0, -110}, + {10, -110}, + {20, -110}, + {30, -110}, + {70, -110}, + {80, -110}, + {90, -110}, + {100, -110}, + {-100, -100}, + {-90, -100}, + {-80, -100}, + {-70, -100}, + {-60, -100}, + {-50, -100}, + {-40, -100}, + {-30, -100}, + {-20, -100}, + {-10, -100}, + {0, -100}, + {10, -100}, + {20, -100}, + {30, -100}, + {40, -100}, + {50, -100}, + {60, -100}, + {70, -100}, + {80, -100}, + {90, -100}, + {100, -100}, + {-110, -90}, + {-100, -90}, + {-90, -90}, + {-80, -90}, + {-70, -90}, + {-60, -90}, + {-50, -90}, + {-40, -90}, + {-30, -90}, + {-20, -90}, + {-10, -90}, + {0, -90}, + {10, -90}, + {20, -90}, + {30, -90}, + {40, -90}, + {50, -90}, + {60, -90}, + {70, -90}, + {80, -90}, + {-120, -80}, + {-110, -80}, + {-100, -80}, + {-90, -80}, + {-80, -80}, + {-70, -80}, + {-60, -80}, + {-50, -80}, + {-40, -80}, + {-30, -80}, + {-20, -80}, + {-10, -80}, + {0, -80}, + {10, -80}, + {20, -80}, + {30, -80}, + {40, -80}, + {50, -80}, + {60, -80}, + {70, -80}, + {80, -80}, + {-120, -70}, + {-110, -70}, + {-100, -70}, + {-90, -70}, + {-80, -70}, + {-70, -70}, + {-60, -70}, + {-50, -70}, + {-40, -70}, + {-30, -70}, + {-20, -70}, + {-10, -70}, + {0, -70}, + {10, -70}, + {20, -70}, + {30, -70}, + {40, -70}, + {50, -70}, + {60, -70}, + {70, -70}, + {80, -70}, + {90, -70}, + {100, -70}, + {110, -70}, + {120, -70}, + {130, -70}, + {140, -70}, + {-130, -60}, + {-120, -60}, + {-110, -60}, + {-100, -60}, + {-90, -60}, + {-80, -60}, + {-70, -60}, + {-60, -60}, + {-50, -60}, + {-40, -60}, + {-30, -60}, + {-20, -60}, + {-10, -60}, + {0, -60}, + {10, -60}, + {20, -60}, + {30, -60}, + {40, -60}, + {50, -60}, + {60, -60}, + {70, -60}, + {80, -60}, + {90, -60}, + {100, -60}, + {110, -60}, + {120, -60}, + {130, -60}, + {-130, -50}, + {-120, -50}, + {-110, -50}, + {-100, -50}, + {-90, -50}, + {-80, -50}, + {-70, -50}, + {-60, -50}, + {-50, -50}, + {-40, -50}, + {-30, -50}, + {-20, -50}, + {-10, -50}, + {0, -50}, + {10, -50}, + {20, -50}, + {30, -50}, + {40, -50}, + {50, -50}, + {60, -50}, + {70, -50}, + {80, -50}, + {90, -50}, + {110, -50}, + {120, -50}, + {-140, -40}, + {-130, -40}, + {-120, -40}, + {-110, -40}, + {-100, -40}, + {-90, -40}, + {-80, -40}, + {-70, -40}, + {-60, -40}, + {-50, -40}, + {-40, -40}, + {-30, -40}, + {-20, -40}, + {-10, -40}, + {0, -40}, + {10, -40}, + {20, -40}, + {30, -40}, + {40, -40}, + {50, -40}, + {60, -40}, + {70, -40}, + {80, -40}, + {90, -40}, + {-140, -30}, + {-130, -30}, + {-120, -30}, + {-110, -30}, + {-100, -30}, + {-90, -30}, + {-80, -30}, + {-70, -30}, + {-60, -30}, + {-50, -30}, + {-40, -30}, + {-30, -30}, + {-20, -30}, + {-10, -30}, + {0, -30}, + {10, -30}, + {20, -30}, + {30, -30}, + {40, -30}, + {50, -30}, + {60, -30}, + {70, -30}, + {80, -30}, + {90, -30}, + {100, -30}, + {-140, -20}, + {-130, -20}, + {-120, -20}, + {-110, -20}, + {-100, -20}, + {-90, -20}, + {-80, -20}, + {-70, -20}, + {-60, -20}, + {-50, -20}, + {-40, -20}, + {-30, -20}, + {-20, -20}, + {-10, -20}, + {0, -20}, + {10, -20}, + {20, -20}, + {30, -20}, + {40, -20}, + {50, -20}, + {60, -20}, + {70, -20}, + {80, -20}, + {90, -20}, + {100, -20}, + {-140, -10}, + {-130, -10}, + {-120, -10}, + {-110, -10}, + {-100, -10}, + {-90, -10}, + {-80, -10}, + {-70, -10}, + {-60, -10}, + {-50, -10}, + {-40, -10}, + {-30, -10}, + {-20, -10}, + {-10, -10}, + {0, -10}, + {10, -10}, + {20, -10}, + {30, -10}, + {40, -10}, + {50, -10}, + {60, -10}, + {70, -10}, + {80, -10}, + {90, -10}, + {100, -10}, + {-140, 0}, + {-130, 0}, + {-120, 0}, + {-110, 0}, + {-100, 0}, + {-90, 0}, + {-80, 0}, + {-70, 0}, + {-60, 0}, + {-50, 0}, + {-40, 0}, + {-30, 0}, + {-20, 0}, + {-10, 0}, + {0, 0}, + {10, 0}, + {20, 0}, + {30, 0}, + {40, 0}, + {50, 0}, + {60, 0}, + {70, 0}, + {80, 0}, + {90, 0}, + {100, 0}, + {-140, 10}, + {-130, 10}, + {-120, 10}, + {-110, 10}, + {-100, 10}, + {-90, 10}, + {-80, 10}, + {-70, 10}, + {-60, 10}, + {-50, 10}, + {-40, 10}, + {-30, 10}, + {-20, 10}, + {-10, 10}, + {0, 10}, + {10, 10}, + {20, 10}, + {30, 10}, + {40, 10}, + {50, 10}, + {60, 10}, + {70, 10}, + {80, 10}, + {90, 10}, + {100, 10}, + {-140, 20}, + {-130, 20}, + {-120, 20}, + {-110, 20}, + {-100, 20}, + {-90, 20}, + {-80, 20}, + {-70, 20}, + {-60, 20}, + {-50, 20}, + {-40, 20}, + {-30, 20}, + {-20, 20}, + {-10, 20}, + {0, 20}, + {10, 20}, + {20, 20}, + {30, 20}, + {40, 20}, + {50, 20}, + {60, 20}, + {70, 20}, + {80, 20}, + {90, 20}, + {100, 20}, + {-140, 30}, + {-130, 30}, + {-120, 30}, + {-110, 30}, + {-100, 30}, + {-90, 30}, + {-80, 30}, + {-70, 30}, + {-60, 30}, + {-50, 30}, + {-40, 30}, + {-30, 30}, + {-20, 30}, + {-10, 30}, + {0, 30}, + {10, 30}, + {20, 30}, + {30, 30}, + {40, 30}, + {50, 30}, + {60, 30}, + {70, 30}, + {80, 30}, + {90, 30}, + {-130, 40}, + {-120, 40}, + {-110, 40}, + {-100, 40}, + {-90, 40}, + {-80, 40}, + {-70, 40}, + {-60, 40}, + {-50, 40}, + {-40, 40}, + {-30, 40}, + {-20, 40}, + {-10, 40}, + {0, 40}, + {10, 40}, + {20, 40}, + {30, 40}, + {40, 40}, + {50, 40}, + {60, 40}, + {70, 40}, + {80, 40}, + {90, 40}, + {-130, 50}, + {-120, 50}, + {-110, 50}, + {-100, 50}, + {-90, 50}, + {-80, 50}, + {-70, 50}, + {-60, 50}, + {-50, 50}, + {-40, 50}, + {-30, 50}, + {-20, 50}, + {-10, 50}, + {0, 50}, + {10, 50}, + {20, 50}, + {30, 50}, + {40, 50}, + {50, 50}, + {60, 50}, + {70, 50}, + {80, 50}, + {-120, 60}, + {-110, 60}, + {-100, 60}, + {-90, 60}, + {-80, 60}, + {-70, 60}, + {-60, 60}, + {-50, 60}, + {-40, 60}, + {-30, 60}, + {-20, 60}, + {-10, 60}, + {0, 60}, + {10, 60}, + {20, 60}, + {30, 60}, + {40, 60}, + {50, 60}, + {60, 60}, + {70, 60}, + {80, 60}, + {-120, 70}, + {-110, 70}, + {-100, 70}, + {-90, 70}, + {-80, 70}, + {-70, 70}, + {-60, 70}, + {-50, 70}, + {-40, 70}, + {-30, 70}, + {-20, 70}, + {-10, 70}, + {0, 70}, + {10, 70}, + {20, 70}, + {30, 70}, + {40, 70}, + {50, 70}, + {60, 70}, + {70, 70}, + {-110, 80}, + {-100, 80}, + {-90, 80}, + {-80, 80}, + {-70, 80}, + {-60, 80}, + {-50, 80}, + {-40, 80}, + {-30, 80}, + {-20, 80}, + {-10, 80}, + {0, 80}, + {10, 80}, + {20, 80}, + {30, 80}, + {40, 80}, + {50, 80}, + {60, 80}, + {-100, 90}, + {-90, 90}, + {-80, 90}, + {-70, 90}, + {-60, 90}, + {-50, 90}, + {-40, 90}, + {-30, 90}, + {-20, 90}, + {-10, 90}, + {0, 90}, + {10, 90}, + {20, 90}, + {30, 90}, + {40, 90}, + {50, 90}, + {-80, 100}, + {-70, 100}, + {-60, 100}, + {-50, 100}, + {-40, 100}, + {-30, 100}, + {-20, 100}, + {-10, 100}, + {0, 100}, + {10, 100}, + {20, 100}, + {30, 100}, + {-60, 110}, + {-50, 110}, + {-40, 110}, + {-30, 110}, + {-20, 110}, + {-10, 110}, + {0, 110}, + {10, 110}, +} + +var imgColorRGB = [501][3]uint8{ + {0xB4, 0xE8, 0xEF}, + {0x92, 0xE0, 0xEA}, + {0x81, 0xDC, 0xE8}, + {0x7B, 0xDB, 0xE7}, + {0x8C, 0xDF, 0xE9}, + {0xA4, 0xE5, 0xED}, + {0xC9, 0xED, 0xF2}, + {0x51, 0xD1, 0xE1}, + {0x1C, 0xC4, 0xDA}, + {0xB0, 0xC0, 0xD7}, + {0x00, 0xBD, 0xD5}, + {0x00, 0xBC, 0xD5}, + {0x00, 0xBC, 0xD5}, + {0x00, 0xBD, 0xD5}, + {0x40, 0xBF, 0xD6}, + {0x13, 0xC2, 0xD8}, + {0x2F, 0xC9, 0xDC}, + {0x99, 0xE2, 0xEB}, + {0xE9, 0xF5, 0xF6}, + {0x27, 0xC7, 0xDB}, + {0x30, 0xC9, 0xDC}, + {0xDC, 0xF2, 0xF5}, + {0xF2, 0xF7, 0xF8}, + {0x6F, 0xD8, 0xE5}, + {0x00, 0xBC, 0xD5}, + {0x00, 0xBD, 0xD5}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBD, 0xD5}, + {0x00, 0xBC, 0xD5}, + {0x2B, 0xC7, 0xDC}, + {0xDC, 0xF2, 0xF5}, + {0xE7, 0xF5, 0xF6}, + {0x15, 0xC3, 0xD8}, + {0x00, 0xBD, 0xD5}, + {0x1F, 0xC5, 0xDA}, + {0xD7, 0xF1, 0xF4}, + {0xE0, 0xF3, 0xF5}, + {0x41, 0xCD, 0xDF}, + {0x00, 0xBC, 0xD4}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBC, 0xD5}, + {0x1A, 0xC4, 0xDA}, + {0x15, 0xC2, 0xD9}, + {0x1C, 0xC4, 0xDA}, + {0x80, 0xDC, 0xE8}, + {0xF2, 0xF7, 0xF8}, + {0x2C, 0xC9, 0xDC}, + {0x00, 0xBC, 0xD5}, + {0x00, 0xBE, 0xD5}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBD, 0xD5}, + {0x00, 0xBB, 0xD4}, + {0x6C, 0xD7, 0xE5}, + {0xCE, 0xEF, 0xF3}, + {0x3D, 0xCD, 0xDF}, + {0x00, 0xBD, 0xD5}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBD, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBD, 0xD5}, + {0xC0, 0xC0, 0xD7}, + {0xE6, 0xF4, 0xF6}, + {0x9B, 0xE3, 0xEC}, + {0x00, 0xBD, 0xD5}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBC, 0xD5}, + {0x00, 0xBC, 0xD4}, + {0x00, 0xBC, 0xD4}, + {0x00, 0xBC, 0xD4}, + {0x00, 0xBC, 0xD4}, + {0x00, 0xBC, 0xD4}, + {0x00, 0xBC, 0xD4}, + {0x00, 0xBC, 0xD4}, + {0x00, 0xBC, 0xD4}, + {0x00, 0xBC, 0xD4}, + {0x00, 0xBC, 0xD4}, + {0x00, 0xBC, 0xD4}, + {0x00, 0xBC, 0xD4}, + {0x00, 0xBC, 0xD4}, + {0x00, 0xBC, 0xD4}, + {0x00, 0xBC, 0xD5}, + {0x00, 0xBD, 0xD5}, + {0x00, 0xBE, 0xD6}, + {0x20, 0xC6, 0xDA}, + {0x67, 0xD6, 0xE4}, + {0x30, 0xC9, 0xDC}, + {0x18, 0xC3, 0xD9}, + {0x28, 0xC7, 0xDB}, + {0xB6, 0xE9, 0xEF}, + {0x37, 0xCB, 0xDD}, + {0x00, 0xBD, 0xD5}, + {0x20, 0xBE, 0xD6}, + {0x4D, 0xD0, 0xE1}, + {0x8F, 0xDF, 0xEA}, + {0x8E, 0xDF, 0xEA}, + {0x8E, 0xDF, 0xEA}, + {0x8E, 0xDF, 0xEA}, + {0x8E, 0xDF, 0xEA}, + {0x8E, 0xDF, 0xEA}, + {0x8E, 0xDF, 0xEA}, + {0x8E, 0xDF, 0xEA}, + {0x8E, 0xDF, 0xEA}, + {0x8E, 0xDF, 0xEA}, + {0x8E, 0xDF, 0xEA}, + {0x8E, 0xDF, 0xEA}, + {0x8E, 0xDF, 0xEA}, + {0x8F, 0xDF, 0xEA}, + {0x76, 0xDA, 0xE6}, + {0xF0, 0xC1, 0xD8}, + {0x00, 0xBE, 0xD6}, + {0x10, 0xC1, 0xD8}, + {0xBE, 0xEB, 0xF0}, + {0xCE, 0xEE, 0xF3}, + {0xDE, 0xF2, 0xF5}, + {0xBB, 0xEA, 0xF0}, + {0xC0, 0xC1, 0xD7}, + {0x00, 0xBD, 0xD5}, + {0x15, 0xC3, 0xD9}, + {0xCD, 0xEF, 0xF3}, + {0xFA, 0xF9, 0xF9}, + {0xF8, 0xF8, 0xF8}, + {0xF8, 0xF8, 0xF8}, + {0xF8, 0xF8, 0xF8}, + {0xF8, 0xF8, 0xF8}, + {0xF8, 0xF8, 0xF8}, + {0xF8, 0xF8, 0xF8}, + {0xF8, 0xF8, 0xF8}, + {0xF8, 0xF8, 0xF8}, + {0xF8, 0xF8, 0xF8}, + {0xF8, 0xF8, 0xF8}, + {0xF8, 0xF8, 0xF8}, + {0xF8, 0xF8, 0xF8}, + {0xF8, 0xF8, 0xF8}, + {0xFF, 0xFB, 0xFA}, + {0x40, 0xCD, 0xDF}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBC, 0xD5}, + {0x75, 0xD9, 0xE6}, + {0x8F, 0xDF, 0xEA}, + {0x00, 0xBB, 0xD4}, + {0x00, 0xBD, 0xD6}, + {0x1E, 0xC5, 0xDA}, + {0xF2, 0xF7, 0xF7}, + {0xF9, 0xF9, 0xF9}, + {0xF9, 0xF9, 0xF9}, + {0xF9, 0xF9, 0xF9}, + {0xF9, 0xF9, 0xF9}, + {0xF9, 0xF9, 0xF9}, + {0xF9, 0xF9, 0xF9}, + {0xF9, 0xF9, 0xF9}, + {0xF9, 0xF9, 0xF9}, + {0xF9, 0xF9, 0xF9}, + {0xF9, 0xF9, 0xF9}, + {0xF9, 0xF9, 0xF9}, + {0xF9, 0xF9, 0xF9}, + {0xF9, 0xF9, 0xF9}, + {0xF8, 0xF9, 0xF9}, + {0xFF, 0xFB, 0xFA}, + {0x6E, 0xD8, 0xE5}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBD, 0xD5}, + {0x3C, 0xCC, 0xDE}, + {0xE8, 0xF5, 0xF6}, + {0x65, 0xD6, 0xE4}, + {0x00, 0xBC, 0xD5}, + {0x00, 0xBD, 0xD5}, + {0x34, 0xCA, 0xDD}, + {0xFF, 0xFC, 0xFB}, + {0xF9, 0xF9, 0xF9}, + {0xFF, 0xFB, 0xFA}, + {0xFF, 0xFC, 0xFB}, + {0xFF, 0xFB, 0xFA}, + {0xF9, 0xF9, 0xF9}, + {0xF9, 0xF9, 0xF9}, + {0xF9, 0xF9, 0xF9}, + {0xF9, 0xF9, 0xF9}, + {0xF9, 0xF9, 0xF9}, + {0xF9, 0xF9, 0xF9}, + {0xFF, 0xFA, 0xFA}, + {0xFF, 0xFC, 0xFB}, + {0xFF, 0xFB, 0xFA}, + {0xFA, 0xF9, 0xF9}, + {0xFF, 0xFA, 0xFA}, + {0x9C, 0xE3, 0xEC}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBD, 0xD5}, + {0x16, 0xC3, 0xD9}, + {0xDF, 0xF2, 0xF5}, + {0x4E, 0xD0, 0xE0}, + {0x00, 0xBC, 0xD5}, + {0x00, 0xBD, 0xD5}, + {0x66, 0xD6, 0xE4}, + {0xFF, 0xFB, 0xFA}, + {0xFD, 0xF9, 0xF9}, + {0x78, 0xDB, 0xE7}, + {0x33, 0xCA, 0xDC}, + {0x7E, 0xDC, 0xE7}, + {0xF4, 0xF7, 0xF8}, + {0xF8, 0xF8, 0xF8}, + {0xF9, 0xF9, 0xF9}, + {0xF9, 0xF9, 0xF9}, + {0xF8, 0xF8, 0xF8}, + {0xF8, 0xF8, 0xF8}, + {0x9A, 0xE2, 0xEB}, + {0x33, 0xCA, 0xDD}, + {0x62, 0xD5, 0xE4}, + {0xF3, 0xF7, 0xF8}, + {0xFE, 0xFA, 0xF9}, + {0xCC, 0xEE, 0xF3}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBD, 0xD5}, + {0x30, 0xBF, 0xD6}, + {0xDA, 0xF1, 0xF4}, + {0x4C, 0xD0, 0xE0}, + {0x00, 0xBC, 0xD5}, + {0x00, 0xBD, 0xD5}, + {0x57, 0xD3, 0xE2}, + {0xFC, 0xF9, 0xF9}, + {0x61, 0xD5, 0xE3}, + {0x00, 0xBC, 0xD5}, + {0x00, 0xBD, 0xD5}, + {0x00, 0xB9, 0xD3}, + {0x7D, 0xDC, 0xE7}, + {0xFD, 0xFA, 0xF9}, + {0xFA, 0xF9, 0xF9}, + {0xFA, 0xF9, 0xF9}, + {0xFD, 0xFA, 0xF9}, + {0xAB, 0xE6, 0xEE}, + {0x00, 0xB9, 0xD3}, + {0x00, 0xBD, 0xD5}, + {0x00, 0xBC, 0xD5}, + {0x38, 0xCB, 0xDD}, + {0xF8, 0xF8, 0xF9}, + {0xB6, 0xE9, 0xEF}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBD, 0xD5}, + {0x40, 0xBF, 0xD6}, + {0xDA, 0xF1, 0xF4}, + {0x62, 0xD5, 0xE3}, + {0x00, 0xBC, 0xD5}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBD, 0xD5}, + {0x20, 0xC5, 0xDA}, + {0x60, 0xBF, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBD, 0xD5}, + {0x1A, 0xC4, 0xD9}, + {0xA0, 0xE4, 0xEC}, + {0xA2, 0xE4, 0xEC}, + {0xA3, 0xE5, 0xEC}, + {0xA6, 0xE5, 0xED}, + {0x38, 0xCB, 0xDD}, + {0x00, 0xBC, 0xD5}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x40, 0xBF, 0xD6}, + {0x2D, 0xC8, 0xDC}, + {0x40, 0xBF, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBD, 0xD5}, + {0x13, 0xC2, 0xD8}, + {0xDE, 0xF2, 0xF5}, + {0x8A, 0xDF, 0xE9}, + {0x00, 0xBC, 0xD4}, + {0x00, 0xBD, 0xD6}, + {0x00, 0xBD, 0xD5}, + {0x00, 0xBD, 0xD5}, + {0x00, 0xBD, 0xD5}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBD, 0xD6}, + {0x00, 0xBD, 0xD5}, + {0x30, 0xBE, 0xD6}, + {0x50, 0xBF, 0xD6}, + {0x50, 0xBF, 0xD6}, + {0x50, 0xBF, 0xD6}, + {0x10, 0xBE, 0xD6}, + {0x00, 0xBD, 0xD5}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBD, 0xD5}, + {0x00, 0xBD, 0xD5}, + {0x00, 0xBD, 0xD5}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBD, 0xD5}, + {0x33, 0xCA, 0xDD}, + {0xE6, 0xF4, 0xF6}, + {0xB6, 0xE9, 0xEF}, + {0x90, 0xC0, 0xD7}, + {0x00, 0xBD, 0xD5}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBC, 0xD5}, + {0x6B, 0xD7, 0xE4}, + {0x31, 0xCA, 0xDC}, + {0x00, 0xBD, 0xD5}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x80, 0xC0, 0xD7}, + {0xAE, 0xE7, 0xEE}, + {0x8D, 0xDF, 0xE9}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x37, 0xCB, 0xDD}, + {0x2A, 0xC8, 0xDC}, + {0x00, 0xBD, 0xD5}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0xC6, 0xED, 0xF2}, + {0xE6, 0xF4, 0xF6}, + {0x1D, 0xC5, 0xDA}, + {0x00, 0xBC, 0xD5}, + {0x00, 0xBE, 0xD5}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBD, 0xD5}, + {0x00, 0xBE, 0xD6}, + {0x93, 0xE0, 0xEA}, + {0xD4, 0xF0, 0xF3}, + {0x2E, 0xC9, 0xDD}, + {0x00, 0xBC, 0xD4}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBD, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBD, 0xD5}, + {0x50, 0xBF, 0xD6}, + {0x98, 0xE2, 0xEB}, + {0xEA, 0xF5, 0xF7}, + {0x4B, 0xD0, 0xE1}, + {0x00, 0xBC, 0xD5}, + {0x00, 0xBD, 0xD5}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBE, 0xD6}, + {0x00, 0xBC, 0xD5}, + {0x14, 0xC2, 0xD8}, + {0xBE, 0xEB, 0xF0}, + {0xAE, 0xE7, 0xEE}, + {0x34, 0xCA, 0xDD}, + {0x13, 0xC2, 0xD8}, + {0x10, 0xBE, 0xD6}, + {0x00, 0xBC, 0xD5}, + {0x00, 0xBD, 0xD5}, + {0x00, 0xBD, 0xD5}, + {0x00, 0xBC, 0xD5}, + {0x00, 0xBD, 0xD5}, + {0xC0, 0xC1, 0xD7}, + {0x1F, 0xC5, 0xDA}, + {0x77, 0xDB, 0xE7}, + {0xCA, 0xED, 0xF2}, + {0x9E, 0xE3, 0xEC}, + {0x80, 0xDC, 0xE8}, + {0x6A, 0xD7, 0xE5}, + {0x6B, 0xD7, 0xE5}, + {0x73, 0xD9, 0xE6}, + {0x92, 0xE1, 0xEA}, + {0xB8, 0xEA, 0xF0}, +} diff --git a/firefly.toml b/firefly.toml new file mode 100644 index 0000000..feb12f3 --- /dev/null +++ b/firefly.toml @@ -0,0 +1,5 @@ +author_id = "conejo" +app_id = "fire-pixel" +author_name = "Conejo" +app_name = "Fire Pixel" +lang = "go" diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..28a9f7c --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module fire-pixel + +go 1.24.0 + +toolchain go1.24.13 + +require github.com/firefly-zero/firefly-go v0.10.0 + +require github.com/orsinium-labs/tinymath v1.0.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..8bdef0c --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +github.com/firefly-zero/firefly-go v0.10.0 h1:CFEVPrXdLCZYgMna2DMncZ+lR01xg3danlXtIw+dJkE= +github.com/firefly-zero/firefly-go v0.10.0/go.mod h1:+X/XGyPdES51OESkV8NSf1mszEBZionoROM7x2pBofw= +github.com/orsinium-labs/tinymath v1.0.0 h1:Uzp3GmjWIBxMObx4MQi9ACDu4Q8WKjSRakB1OMo9Bu0= +github.com/orsinium-labs/tinymath v1.0.0/go.mod h1:WPXX6ei3KSXG7JfA03a+ekCYaY9SWN4I+JRl2p6ck+A= diff --git a/main.go b/main.go new file mode 100644 index 0000000..5e393d7 --- /dev/null +++ b/main.go @@ -0,0 +1,166 @@ +package main + +import ( + "github.com/firefly-zero/firefly-go/firefly" + "github.com/orsinium-labs/tinymath" +) + +const ( + screenW = 240 + screenH = 160 + pixSize = 3 + projDist = 10000.0 + scale = 0.45 + rotSpeed = 0.0015 +) + +// Custom palette for the ninja sprite (cyan gradient + accents) +var paletteRGB = [16][3]uint8{ + {0x00, 0xBC, 0xD5}, // 1: darkest cyan + {0x15, 0xC3, 0xD8}, // 2: dark cyan + {0x30, 0xC9, 0xDC}, // 3: medium-dark cyan + {0x50, 0xD1, 0xE1}, // 4: medium cyan + {0x70, 0xD8, 0xE5}, // 5: medium-light cyan + {0x90, 0xE0, 0xEA}, // 6: light cyan + {0xA8, 0xE5, 0xED}, // 7: pale cyan + {0xC0, 0xEB, 0xF0}, // 8: very pale cyan + {0xD8, 0xF1, 0xF4}, // 9: ice cyan + {0xE8, 0xF5, 0xF6}, // 10: near-white cyan + {0xF2, 0xF7, 0xF8}, // 11: almost white + {0xF9, 0xF9, 0xF9}, // 12: white + {0xFF, 0xFB, 0xFA}, // 13: warm white + {0xF0, 0xC1, 0xD8}, // 14: pink accent + {0xB0, 0xC0, 0xD7}, // 15: blue-gray + {0xC0, 0xC1, 0xD7}, // 16: light blue-gray +} + +type pixel3D struct { + x, y, z float32 + color firefly.Color + screenX int + screenY int + screenZ float32 + pixScale float32 +} + +var ( + pixels [numPixels]pixel3D + angleX float32 + angleY float32 +) + +func init() { + firefly.Boot = boot + firefly.Update = update + firefly.Render = render +} + +func boot() { + // Set custom palette + var pal [16]firefly.RGB + for i := 0; i < 16; i++ { + pal[i] = firefly.NewRGB(paletteRGB[i][0], paletteRGB[i][1], paletteRGB[i][2]) + } + firefly.SetPalette(pal) + + // Initialize pixels with coordinates, random Z, and nearest palette color + for i := 0; i < numPixels; i++ { + pixels[i].x = float32(imgCoord[i][0]) * scale + pixels[i].y = float32(imgCoord[i][1]) * scale + // Random Z between -45 and 45 (scaled from original -100..100) + rnd := firefly.GetRandom() + pixels[i].z = (float32(rnd%2000) - 1000.0) / 1000.0 * 45.0 + pixels[i].color = nearestPaletteColor(imgColorRGB[i][0], imgColorRGB[i][1], imgColorRGB[i][2]) + } +} + +func nearestPaletteColor(r, g, b uint8) firefly.Color { + bestDist := int32(1 << 30) + bestIdx := firefly.Color(1) + for i := 0; i < 16; i++ { + dr := int32(r) - int32(paletteRGB[i][0]) + dg := int32(g) - int32(paletteRGB[i][1]) + db := int32(b) - int32(paletteRGB[i][2]) + dist := dr*dr + dg*dg + db*db + if dist < bestDist { + bestDist = dist + bestIdx = firefly.Color(i + 1) + } + } + return bestIdx +} + +func update() { + pad, _ := firefly.ReadPad(firefly.Combined) + // Map pad position (-1000..1000) to rotation angle + angleX += float32(pad.X) * rotSpeed + angleY += float32(pad.Y) * rotSpeed + + // Project all pixels to 2D + radX := angleX * (tinymath.Pi / 180.0) + radY := angleY * (tinymath.Pi / 180.0) + sinX, cosX := tinymath.SinCos(radX) + sinY, cosY := tinymath.SinCos(radY) + + for i := 0; i < numPixels; i++ { + px := pixels[i].x + py := pixels[i].y + pz := pixels[i].z + + // Rotate around Y axis (horizontal mouse/pad movement) + rx := px*cosX - pz*sinX + rz1 := px*sinX + pz*cosX + + // Rotate around X axis (vertical mouse/pad movement) + ry := py*cosY - rz1*sinY + rz := py*sinY + rz1*cosY + + // Perspective projection + d := projDist + rz + if d < 100 { + d = 100 + } + sx := projDist * rx / d + sy := projDist * ry / d + + // Scale factor based on depth + s := 1.0 + (pixels[i].z-rz)*0.004 + if s < 0.3 { + s = 0.3 + } + if s > 2.5 { + s = 2.5 + } + + pixels[i].screenX = int(sx) + screenW/2 + pixels[i].screenY = int(sy) + screenH/2 + pixels[i].screenZ = rz + pixels[i].pixScale = s + } +} + +func render() { + firefly.ClearScreen(firefly.ColorWhite) + + // Draw all pixels as small rectangles + for i := 0; i < numPixels; i++ { + p := &pixels[i] + sz := int(float32(pixSize) * p.pixScale) + if sz < 1 { + sz = 1 + } + x := p.screenX - sz/2 + y := p.screenY - sz/2 + + // Skip if off-screen + if x+sz < 0 || x >= screenW || y+sz < 0 || y >= screenH { + continue + } + + firefly.DrawRect( + firefly.Point{X: x, Y: y}, + firefly.Size{W: sz, H: sz}, + firefly.Solid(p.color), + ) + } +} diff --git a/pixeljs-master/converter.php b/pixeljs-master/converter.php new file mode 100644 index 0000000..f9d20fa --- /dev/null +++ b/pixeljs-master/converter.php @@ -0,0 +1,56 @@ + $v) { + + $color[] = $r[$k].$g[$k].$b[$k]; + $coord[] = "[".$xx[$k].",".$yy[$k]."]"; +} + +echo "var img_color = ['".implode("','", $color)."'];\n\n"; +echo "var img_coord = [".implode(",", $coord)."];"; + +?> diff --git a/pixeljs-master/img/ninja.js b/pixeljs-master/img/ninja.js new file mode 100644 index 0000000..b8f94b4 --- /dev/null +++ b/pixeljs-master/img/ninja.js @@ -0,0 +1,3 @@ +var img_color = ['B4E8EF','92E0EA','81DCE8','7BDBE7','8CDFE9','A4E5ED','C9EDF2','51D1E1','1CC4DA','B0C0D7','00BDD5','00BCD5','00BCD5','00BDD5','40BFD6','13C2D8','2FC9DC','99E2EB','E9F5F6','27C7DB','30C9DC','DCF2F5','F2F7F8','6FD8E5','00BCD5','00BDD5','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BDD5','00BCD5','2BC7DC','DCF2F5','E7F5F6','15C3D8','00BDD5','1FC5DA','D7F1F4','E0F3F5','41CDDF','00BCD4','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BCD5','1AC4DA','15C2D9','1CC4DA','80DCE8','F2F7F8','2CC9DC','00BCD5','00BED5','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BDD5','00BBD4','6CD7E5','CEEFF3','3DCDDF','00BDD5','00BED6','00BDD6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BDD5','C0C0D7','E6F4F6','9BE3EC','00BDD5','00BED6','00BCD5','00BCD4','00BCD4','00BCD4','00BCD4','00BCD4','00BCD4','00BCD4','00BCD4','00BCD4','00BCD4','00BCD4','00BCD4','00BCD4','00BCD4','00BCD5','00BDD5','00BED6','20C6DA','67D6E4','30C9DC','18C3D9','28C7DB','B6E9EF','37CBDD','00BDD5','20BED6','4DD0E1','8FDFEA','8EDFEA','8EDFEA','8EDFEA','8EDFEA','8EDFEA','8EDFEA','8EDFEA','8EDFEA','8EDFEA','8EDFEA','8EDFEA','8EDFEA','8FDFEA','76DAE6','F0C1D8','00BED6','10C1D8','BEEBF0','CEEEF3','DEF2F5','BBEAF0','C0C1D7','00BDD5','15C3D9','CDEFF3','FAF9F9','F8F8F8','F8F8F8','F8F8F8','F8F8F8','F8F8F8','F8F8F8','F8F8F8','F8F8F8','F8F8F8','F8F8F8','F8F8F8','F8F8F8','F8F8F8','FFFBFA','40CDDF','00BED6','00BCD5','75D9E6','8FDFEA','00BBD4','00BDD6','1EC5DA','F2F7F7','F9F9F9','F9F9F9','F9F9F9','F9F9F9','F9F9F9','F9F9F9','F9F9F9','F9F9F9','F9F9F9','F9F9F9','F9F9F9','F9F9F9','F9F9F9','F8F9F9','FFFBFA','6ED8E5','00BED6','00BDD5','3CCCDE','E8F5F6','65D6E4','00BCD5','00BDD5','34CADD','FFFCFB','F9F9F9','FFFBFA','FFFCFB','FFFBFA','F9F9F9','F9F9F9','F9F9F9','F9F9F9','F9F9F9','F9F9F9','FFFAFA','FFFCFB','FFFBFA','FAF9F9','FFFAFA','9CE3EC','00BED6','00BDD5','16C3D9','DFF2F5','4ED0E0','00BCD5','00BDD5','66D6E4','FFFBFA','FDF9F9','78DBE7','33CADC','7EDCE7','F4F7F8','F8F8F8','F9F9F9','F9F9F9','F8F8F8','F8F8F8','9AE2EB','33CADD','62D5E4','F3F7F8','FEFAF9','CCEEF3','00BED6','00BDD5','30BFD6','DAF1F4','4CD0E0','00BCD5','00BDD5','57D3E2','FCF9F9','61D5E3','00BCD5','00BDD5','00B9D3','7DDCE7','FDFAF9','FAF9F9','FAF9F9','FDFAF9','ABE6EE','00B9D3','00BDD5','00BCD5','38CBDD','F8F8F9','B6E9EF','00BED6','00BDD5','40BFD6','DAF1F4','62D5E3','00BCD5','00BED6','00BDD5','20C5DA','60BFD6','00BED6','00BED6','00BDD5','1AC4D9','A0E4EC','A2E4EC','A3E5EC','A6E5ED','38CBDD','00BCD5','00BED6','00BED6','40BFD6','2DC8DC','40BFD6','00BED6','00BDD5','13C2D8','DEF2F5','8ADFE9','00BCD4','00BDD6','00BDD5','00BDD5','00BDD5','00BED6','00BED6','00BDD6','00BDD5','30BED6','50BFD6','50BFD6','50BFD6','10BED6','00BDD5','00BED6','00BED6','00BDD5','00BDD5','00BDD5','00BED6','00BDD5','33CADD','E6F4F6','B6E9EF','90C0D7','00BDD5','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BCD5','6BD7E4','31CADC','00BDD5','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','80C0D7','AEE7EE','8DDFE9','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','37CBDD','2AC8DC','00BDD5','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','C6EDF2','E6F4F6','1DC5DA','00BCD5','00BED5','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BDD5','00BED6','93E0EA','D4F0F3','2EC9DD','00BCD4','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BDD6','00BED6','00BDD5','50BFD6','98E2EB','EAF5F7','4BD0E1','00BCD5','00BDD5','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BED6','00BCD5','14C2D8','BEEBF0','AEE7EE','34CADD','13C2D8','10BED6','00BCD5','00BDD5','00BDD5','00BCD5','00BDD5','C0C1D7','1FC5DA','77DBE7','CAEDF2','9EE3EC','80DCE8','6AD7E5','6BD7E5','73D9E6','92E1EA','B8EAF0']; + +var img_coord = [[-50,-120],[-40,-120],[-30,-120],[-20,-120],[-10,-120],[0,-120],[10,-120],[-70,-110],[-60,-110],[-50,-110],[-40,-110],[-30,-110],[-20,-110],[-10,-110],[0,-110],[10,-110],[20,-110],[30,-110],[70,-110],[80,-110],[90,-110],[100,-110],[-100,-100],[-90,-100],[-80,-100],[-70,-100],[-60,-100],[-50,-100],[-40,-100],[-30,-100],[-20,-100],[-10,-100],[0,-100],[10,-100],[20,-100],[30,-100],[40,-100],[50,-100],[60,-100],[70,-100],[80,-100],[90,-100],[100,-100],[-110,-90],[-100,-90],[-90,-90],[-80,-90],[-70,-90],[-60,-90],[-50,-90],[-40,-90],[-30,-90],[-20,-90],[-10,-90],[0,-90],[10,-90],[20,-90],[30,-90],[40,-90],[50,-90],[60,-90],[70,-90],[80,-90],[-120,-80],[-110,-80],[-100,-80],[-90,-80],[-80,-80],[-70,-80],[-60,-80],[-50,-80],[-40,-80],[-30,-80],[-20,-80],[-10,-80],[0,-80],[10,-80],[20,-80],[30,-80],[40,-80],[50,-80],[60,-80],[70,-80],[80,-80],[-120,-70],[-110,-70],[-100,-70],[-90,-70],[-80,-70],[-70,-70],[-60,-70],[-50,-70],[-40,-70],[-30,-70],[-20,-70],[-10,-70],[0,-70],[10,-70],[20,-70],[30,-70],[40,-70],[50,-70],[60,-70],[70,-70],[80,-70],[90,-70],[100,-70],[110,-70],[120,-70],[130,-70],[140,-70],[-130,-60],[-120,-60],[-110,-60],[-100,-60],[-90,-60],[-80,-60],[-70,-60],[-60,-60],[-50,-60],[-40,-60],[-30,-60],[-20,-60],[-10,-60],[0,-60],[10,-60],[20,-60],[30,-60],[40,-60],[50,-60],[60,-60],[70,-60],[80,-60],[90,-60],[100,-60],[110,-60],[120,-60],[130,-60],[-130,-50],[-120,-50],[-110,-50],[-100,-50],[-90,-50],[-80,-50],[-70,-50],[-60,-50],[-50,-50],[-40,-50],[-30,-50],[-20,-50],[-10,-50],[0,-50],[10,-50],[20,-50],[30,-50],[40,-50],[50,-50],[60,-50],[70,-50],[80,-50],[90,-50],[110,-50],[120,-50],[-140,-40],[-130,-40],[-120,-40],[-110,-40],[-100,-40],[-90,-40],[-80,-40],[-70,-40],[-60,-40],[-50,-40],[-40,-40],[-30,-40],[-20,-40],[-10,-40],[0,-40],[10,-40],[20,-40],[30,-40],[40,-40],[50,-40],[60,-40],[70,-40],[80,-40],[90,-40],[-140,-30],[-130,-30],[-120,-30],[-110,-30],[-100,-30],[-90,-30],[-80,-30],[-70,-30],[-60,-30],[-50,-30],[-40,-30],[-30,-30],[-20,-30],[-10,-30],[0,-30],[10,-30],[20,-30],[30,-30],[40,-30],[50,-30],[60,-30],[70,-30],[80,-30],[90,-30],[100,-30],[-140,-20],[-130,-20],[-120,-20],[-110,-20],[-100,-20],[-90,-20],[-80,-20],[-70,-20],[-60,-20],[-50,-20],[-40,-20],[-30,-20],[-20,-20],[-10,-20],[0,-20],[10,-20],[20,-20],[30,-20],[40,-20],[50,-20],[60,-20],[70,-20],[80,-20],[90,-20],[100,-20],[-140,-10],[-130,-10],[-120,-10],[-110,-10],[-100,-10],[-90,-10],[-80,-10],[-70,-10],[-60,-10],[-50,-10],[-40,-10],[-30,-10],[-20,-10],[-10,-10],[0,-10],[10,-10],[20,-10],[30,-10],[40,-10],[50,-10],[60,-10],[70,-10],[80,-10],[90,-10],[100,-10],[-140,0],[-130,0],[-120,0],[-110,0],[-100,0],[-90,0],[-80,0],[-70,0],[-60,0],[-50,0],[-40,0],[-30,0],[-20,0],[-10,0],[0,0],[10,0],[20,0],[30,0],[40,0],[50,0],[60,0],[70,0],[80,0],[90,0],[100,0],[-140,10],[-130,10],[-120,10],[-110,10],[-100,10],[-90,10],[-80,10],[-70,10],[-60,10],[-50,10],[-40,10],[-30,10],[-20,10],[-10,10],[0,10],[10,10],[20,10],[30,10],[40,10],[50,10],[60,10],[70,10],[80,10],[90,10],[100,10],[-140,20],[-130,20],[-120,20],[-110,20],[-100,20],[-90,20],[-80,20],[-70,20],[-60,20],[-50,20],[-40,20],[-30,20],[-20,20],[-10,20],[0,20],[10,20],[20,20],[30,20],[40,20],[50,20],[60,20],[70,20],[80,20],[90,20],[100,20],[-140,30],[-130,30],[-120,30],[-110,30],[-100,30],[-90,30],[-80,30],[-70,30],[-60,30],[-50,30],[-40,30],[-30,30],[-20,30],[-10,30],[0,30],[10,30],[20,30],[30,30],[40,30],[50,30],[60,30],[70,30],[80,30],[90,30],[-130,40],[-120,40],[-110,40],[-100,40],[-90,40],[-80,40],[-70,40],[-60,40],[-50,40],[-40,40],[-30,40],[-20,40],[-10,40],[0,40],[10,40],[20,40],[30,40],[40,40],[50,40],[60,40],[70,40],[80,40],[90,40],[-130,50],[-120,50],[-110,50],[-100,50],[-90,50],[-80,50],[-70,50],[-60,50],[-50,50],[-40,50],[-30,50],[-20,50],[-10,50],[0,50],[10,50],[20,50],[30,50],[40,50],[50,50],[60,50],[70,50],[80,50],[-120,60],[-110,60],[-100,60],[-90,60],[-80,60],[-70,60],[-60,60],[-50,60],[-40,60],[-30,60],[-20,60],[-10,60],[0,60],[10,60],[20,60],[30,60],[40,60],[50,60],[60,60],[70,60],[80,60],[-120,70],[-110,70],[-100,70],[-90,70],[-80,70],[-70,70],[-60,70],[-50,70],[-40,70],[-30,70],[-20,70],[-10,70],[0,70],[10,70],[20,70],[30,70],[40,70],[50,70],[60,70],[70,70],[-110,80],[-100,80],[-90,80],[-80,80],[-70,80],[-60,80],[-50,80],[-40,80],[-30,80],[-20,80],[-10,80],[0,80],[10,80],[20,80],[30,80],[40,80],[50,80],[60,80],[-100,90],[-90,90],[-80,90],[-70,90],[-60,90],[-50,90],[-40,90],[-30,90],[-20,90],[-10,90],[0,90],[10,90],[20,90],[30,90],[40,90],[50,90],[-80,100],[-70,100],[-60,100],[-50,100],[-40,100],[-30,100],[-20,100],[-10,100],[0,100],[10,100],[20,100],[30,100],[-60,110],[-50,110],[-40,110],[-30,110],[-20,110],[-10,110],[0,110],[10,110]]; \ No newline at end of file diff --git a/pixeljs-master/img/ninja.png b/pixeljs-master/img/ninja.png new file mode 100644 index 0000000000000000000000000000000000000000..da812ec05a6bf9da33f028168274abd96ac467ac GIT binary patch literal 4160 zcmV-G5WnwOz@Z0f2-7z;ux~O9+4z06=<WDR*FRcSTFz- zW=q650N5=6FiBTtNC2?60Km==3$g$R3;-}uh=nNt1bYBr$Ri_o0EC$U6h`t_Jn<{8 z5a%iY0C<_QJh>z}MS)ugEpZ1|S1ukX&Pf+56gFW3VVXcL!g-k)GJ!M?;PcD?0HBc- z5#WRK{dmp}uFlRjj{U%*%WZ25jX z{P*?XzTzZ-GF^d31o+^>%=Ap99M6&ogks$0k4OBs3;+Bb(;~!4V!2o<6ys46agIcq zjPo+3B8fthDa9qy|77CdEc*jK-!%ZRYCZvbku9iQV*~a}ClFY4z~c7+0P?$U!PF=S z1Au6Q;m>#f??3%Vpd|o+W=WE9003S@Bra6Svp>fO002awfhw>;8}z{#EWidF!3EsG z3;bXU&9EIRU@z1_9W=mEXoiz;4lcq~xDGvV5BgyU zp1~-*fe8db$Osc*A=-!mVv1NJjtCc-h4>-CNCXm#Bp}I%6j35eku^v$Qi@a{RY)E3 zJ#qp$hg?Rwkvqr$GJ^buyhkyVfwECO)C{#lxu`c9ghrwZ&}4KmnvWKso6vH!8a<3Q zq36)6Xb;+tK10Vaz~~qUGsJ8#F2=(`u{bOVlVi)VBCHIn#u~6ztOL7=^<&SmcLWlF zMZgI*1b0FpVIDz9SWH+>*hr`#93(Um+6gxa1B6k+CnA%mOSC4s5&6UzVlpv@SV$}* z))J2sFA#f(L&P^E5{W}HC%KRUNwK6<(h|}}(r!{C=`5+6G)NjFlgZj-YqAG9lq?`C z$c5yc>d>VnA`E_*3F2Qp##d8RZb=H01_mm@+|Cqnc9PsG(F5HIG_C zt)aG3uTh7n6Et<2In9F>NlT@zqLtGcXcuVrX|L#Xx)I%#9!{6gSJKPrN9dR61N3(c z4Tcqi$B1Vr8Jidf7-t!G7_XR2rWwr)$3XQ?}=hpK0&Z&W{| zep&sA23f;Q!%st`QJ}G3cbou<7-yIK2z4nfCCCtN2-XOGSWo##{8Q{ATurxr~;I`ytDs%xbip}RzP zziy}Qn4Z2~fSycmr`~zJ=lUFdFa1>gZThG6M+{g7vkW8#+YHVaJjFF}Z#*3@$J_By zLtVo_L#1JrVVB{Ak-5=4qt!-@Mh}c>#$4kh<88)m#-k<%CLtzEP3leVno>={htGUuD;o7bD)w_sX$S}eAxwzy?UvgBH(S?;#HZiQMoS*2K2 zT3xe7t(~nU*1N5{rxB;QPLocnp4Ml>u<^FZwyC!nu;thW+pe~4wtZn|Vi#w(#jeBd zlf9FDx_yoPJqHbk*$%56S{;6Kv~mM9!g3B(KJ}#RZ#@)!hR|78Dq|Iq-afF%KE1Brn_fm;Im z_u$xr8UFki1L{Ox>G0o)(&RAZ;=|I=wN2l97;cLaHH6leTB-XXa*h%dBOEvi`+x zi?=Txl?TadvyiL>SuF~-LZ;|cS}4~l2eM~nS7yJ>iOM;atDY;(?aZ^v+mJV$@1Ote z62cPUlD4IWOIIx&SmwQ~YB{nzae3Pc;}r!fhE@iwJh+OsDs9zItL;~pu715HdQEGA zUct(O!LkCy1<%NCg+}G`0PgpNm-?d@-hMgNe6^V+j6x$b<6@S<$+<4_1hi}Ti zncS4LsjI}fWY1>OX6feMEuLErma3QLmkw?X+1j)X-&VBk_4Y;EFPF_I+q;9dL%E~B zJh;4Nr^(LEJ3myURP{Rblsw%57T)g973R8o)DE9*xN#~;4_o$q%o z4K@u`jhx2fBXC4{U8Qn{*%*B$Ge=nny$HAYq{=vy|sI0 z_vss+H_qMky?OB#|JK!>IX&II^LlUh#rO5!7TtbwC;iULyV-Xq?ybB}ykGP{?LpZ? z-G|jbTmIbG@7#ZCz;~eY(cDM(28Dyq{*m>M4?_iynUBkc4TkHUI6gT!;y-fz>HMcd z&t%Ugo)`Y2{>!cx7B7DI)$7;J(U{Spm-3gBzioV_{p!H$8L!*M!p0uH$#^p{Ui4P` z?ZJ24cOCDe-w#jZd?0@)|7iKK^;6KN`;!@ylm7$*nDhK&GcDTy000JJOGiWi{{a60 z|De66lK=n!32;bRa{vGf6951U69E94oEQKA00(qQO+^RX2n!M)9EhNw`v3q1a!Eu% zR7l6IR$FWpRTN$OoS8XuAEggkXlqLWMcyre8qpXMHPILiKYqZ6#vnrCBQZe(!NhcA15O}gX7pys%^<)2f0fs^YpBd$rK(%9Gfoo$$ zQgu3#zV{>kS}AoL079+b9t@*bB1SaZk-Ma$b`=9IcGT-I})ZhzY_si)&D?tW8R(eUpgC z33HwYLG+q7{O0RjgLuDZIB!AMOiUik00T=Ciho#ILkUS}Z0*KHKjH@0ro+o>G2gYf zDy7~j5Eth?)cV~XKismRGsQ=SvVaiRVVnZPsC8`whM@%Ex#}5s^^USAA*lnvMg!=; za1KQ03L6JnZ_OFrb$?qAw`VjUK+{NI`qLZ6OLgUVwt5C!qA(_CxYCv;E=43H=^!-y z1OQs=Mn{Mr*R=J_IW|0+Pc7q?c&`^!QVMi7H^A~dPdbvG-q)9&b8L8&K}0YNfG31> zQ=oz_=DHRhEsJwY#-n~e;xbFnd-E$%VT+^0zOG0GQPWtPA^*Zw4A-ewubBW+nf`S+*~05sE<*x68l z<#7jwZ~n+i3jChYIGWC3IEZlQ(kND!xb00zXDu^ApG{gRHxdEkvG%Nw%YFoa5EFz2 zu9g4AL^t(}mtAARDWu@HjE9lP+$$ZGvXr_6Py_%%GdFkS{4poSEL)<)wg3Q*AUIOM znWm~7ny?YbOQSKN3hSM@fO8Q;YuyZ>P`bh}*I)3%g5k=f0+?X|*nV~p&8=P7(KU#` zFt{l7%=!1d{b@9}c4K2_3feFjj1@)1gJDEL-gaJbz!m^9MIX|UL1Y-4{z>8Uz7c?c z6GJ&LfSq$H5RGwLf^c9Ui_NF{0ZZV-h=-0*jibx!kP5;p?qnjGBp6E&&d;*t!m(9+ znaZGL#6zzi0^=#R1U~H_!JV!RF?NGW5I*P~2BgS;AmI3AjeGt)hf_JfKu{_LTSNq) zcM`Ef)Ve6{EX>z!p6IL>jE7l>yH|(U*qK7{?+&M2GNECd%<4(Lb6uNUL15Q&dwvxXQhX#Gb?ypc0C1FQG0b~^SyqdB*XBZ#xWx_xfIpmHiTg@pt=Y)z zxtf{qJP!ac6quc}9l8I|(i&@xQp)X}u9Foo9)Pd)^R^V* zzq^>Jf9u>3&UgVXg@)NM6+H4a%t=dNrX^|ptP;F2ry{3_e#%B>Lo()Oi^r;IiHni* zyalcl>kad|6B)g + + + + + + + + +
+ + diff --git a/pixeljs-master/kinetic-v3.10.5.js b/pixeljs-master/kinetic-v3.10.5.js new file mode 100644 index 0000000..70b40e7 --- /dev/null +++ b/pixeljs-master/kinetic-v3.10.5.js @@ -0,0 +1,6600 @@ +/** + * KineticJS JavaScript Library v3.10.5 + * http://www.kineticjs.com/ + * Copyright 2012, Eric Rowell + * Licensed under the MIT or GPL Version 2 licenses. + * Date: Aug 02 2012 + * + * Copyright (C) 2011 - 2012 by Eric Rowell + * + * 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. + */ + +/////////////////////////////////////////////////////////////////////// +// Global +/////////////////////////////////////////////////////////////////////// +/** + * Kinetic Namespace + * @namespace + */ +var Kinetic = {}; +Kinetic.Filters = {}; +Kinetic.Plugins = {}; +Kinetic.Global = { + BUBBLE_WHITELIST: ['mousedown', 'mousemove', 'mouseup', 'mouseover', 'mouseout', 'click', 'dblclick', 'touchstart', 'touchmove', 'touchend', 'tap', 'dbltap', 'dragstart', 'dragmove', 'dragend'], + stages: [], + idCounter: 0, + tempNodes: {}, + maxDragTimeInterval: 20, + drag: { + moving: false, + offset: { + x: 0, + y: 0 + }, + lastDrawTime: 0 + }, + warn: function(str) { + if(console && console.warn) { + console.warn('Kinetic warning: ' + str); + } + }, + _pullNodes: function(stage) { + var tempNodes = this.tempNodes; + for(var key in tempNodes) { + var node = tempNodes[key]; + if(node.getStage() !== undefined && node.getStage()._id === stage._id) { + stage._addId(node); + stage._addName(node); + this._removeTempNode(node); + } + } + }, + _addTempNode: function(node) { + this.tempNodes[node._id] = node; + }, + _removeTempNode: function(node) { + delete this.tempNodes[node._id]; + } +}; + +/////////////////////////////////////////////////////////////////////// +// Transition +/////////////////////////////////////////////////////////////////////// +/** + * Transition constructor. The transitionTo() Node method + * returns a reference to the transition object which you can use + * to stop, resume, or restart the transition + * @constructor + */ +Kinetic.Transition = function(node, config) { + this.node = node; + this.config = config; + this.tweens = []; + var that = this; + + // add tween for each property + function addTween(c, attrs, obj, rootObj) { + for(var key in c) { + if(key !== 'duration' && key !== 'easing' && key !== 'callback') { + // if val is an object then traverse + if(Kinetic.Type._isObject(c[key])) { + obj[key] = {}; + addTween(c[key], attrs[key], obj[key], rootObj); + } + else { + that._add(that._getTween(attrs, key, c[key], obj, rootObj)); + } + } + } + } + var obj = {}; + addTween(config, node.attrs, obj, obj); + + var finishedTweens = 0; + for(var n = 0; n < this.tweens.length; n++) { + var tween = this.tweens[n]; + tween.onFinished = function() { + finishedTweens++; + if(finishedTweens >= that.tweens.length) { + that.onFinished(); + } + }; + } +}; +/* + * Transition methods + */ +Kinetic.Transition.prototype = { + /** + * start transition + * @name start + * @methodOf Kinetic.Transition.prototype + */ + start: function() { + for(var n = 0; n < this.tweens.length; n++) { + this.tweens[n].start(); + } + }, + /** + * stop transition + * @name stop + * @methodOf Kinetic.Transition.prototype + */ + stop: function() { + for(var n = 0; n < this.tweens.length; n++) { + this.tweens[n].stop(); + } + }, + /** + * resume transition + * @name resume + * @methodOf Kinetic.Transition.prototype + */ + resume: function() { + for(var n = 0; n < this.tweens.length; n++) { + this.tweens[n].resume(); + } + }, + _onEnterFrame: function() { + for(var n = 0; n < this.tweens.length; n++) { + this.tweens[n].onEnterFrame(); + } + }, + _add: function(tween) { + this.tweens.push(tween); + }, + _getTween: function(attrs, prop, val, obj, rootObj) { + var config = this.config; + var node = this.node; + var easing = config.easing; + if(easing === undefined) { + easing = 'linear'; + } + + var tween = new Kinetic.Tween(node, function(i) { + obj[prop] = i; + node.setAttrs(rootObj); + }, Kinetic.Tweens[easing], attrs[prop], val, config.duration); + + return tween; + } +}; + +Kinetic.Filters.Grayscale = function() { + var data = this.imageData.data; + for(var i = 0; i < data.length; i += 4) { + var brightness = 0.34 * data[i] + 0.5 * data[i + 1] + 0.16 * data[i + 2]; + // red + data[i] = brightness; + // green + data[i + 1] = brightness; + // blue + data[i + 2] = brightness; + // i+3 is alpha (the fourth element) + } +}; + +/////////////////////////////////////////////////////////////////////// +// Type +/////////////////////////////////////////////////////////////////////// +/* + * utilities that determine data type and transform + * one type into another + */ +Kinetic.Type = { + /* + * cherry-picked utilities from underscore.js + */ + _isElement: function(obj) { + return !!(obj && obj.nodeType == 1); + }, + _isFunction: function(obj) { + return !!(obj && obj.constructor && obj.call && obj.apply); + }, + _isObject: function(obj) { + return (!!obj && obj.constructor == Object); + }, + _isArray: function(obj) { + return Object.prototype.toString.call(obj) == '[object Array]'; + }, + _isNumber: function(obj) { + return Object.prototype.toString.call(obj) == '[object Number]'; + }, + _isString: function(obj) { + return Object.prototype.toString.call(obj) == '[object String]'; + }, + /* + * other utils + */ + _hasMethods: function(obj) { + var names = []; + for(var key in obj) { + if(this._isFunction(obj[key])) + names.push(key); + } + return names.length > 0; + }, + /* + * The argument can be: + * - an integer (will be applied to both x and y) + * - an array of one integer (will be applied to both x and y) + * - an array of two integers (contains x and y) + * - an array of four integers (contains x, y, width, and height) + * - an object with x and y properties + * - an array of one element which is an array of integers + * - an array of one element of an object + */ + _getXY: function(arg) { + if(this._isNumber(arg)) { + return { + x: arg, + y: arg + }; + } + else if(this._isArray(arg)) { + // if arg is an array of one element + if(arg.length === 1) { + var val = arg[0]; + // if arg is an array of one element which is a number + if(this._isNumber(val)) { + return { + x: val, + y: val + }; + } + // if arg is an array of one element which is an array + else if(this._isArray(val)) { + return { + x: val[0], + y: val[1] + }; + } + // if arg is an array of one element which is an object + else if(this._isObject(val)) { + return val; + } + } + // if arg is an array of two or more elements + else if(arg.length >= 2) { + return { + x: arg[0], + y: arg[1] + }; + } + } + // if arg is an object return the object + else if(this._isObject(arg)) { + return arg; + } + + // default + return { + x: 0, + y: 0 + }; + }, + /* + * The argument can be: + * - an integer (will be applied to both width and height) + * - an array of one integer (will be applied to both width and height) + * - an array of two integers (contains width and height) + * - an array of four integers (contains x, y, width, and height) + * - an object with width and height properties + * - an array of one element which is an array of integers + * - an array of one element of an object + */ + _getSize: function(arg) { + if(this._isNumber(arg)) { + return { + width: arg, + height: arg + }; + } + else if(this._isArray(arg)) { + // if arg is an array of one element + if(arg.length === 1) { + var val = arg[0]; + // if arg is an array of one element which is a number + if(this._isNumber(val)) { + return { + width: val, + height: val + }; + } + // if arg is an array of one element which is an array + else if(this._isArray(val)) { + /* + * if arg is an array of one element which is an + * array of four elements + */ + if(val.length >= 4) { + return { + width: val[2], + height: val[3] + }; + } + /* + * if arg is an array of one element which is an + * array of two elements + */ + else if(val.length >= 2) { + return { + width: val[0], + height: val[1] + }; + } + } + // if arg is an array of one element which is an object + else if(this._isObject(val)) { + return val; + } + } + // if arg is an array of four elements + else if(arg.length >= 4) { + return { + width: arg[2], + height: arg[3] + }; + } + // if arg is an array of two elements + else if(arg.length >= 2) { + return { + width: arg[0], + height: arg[1] + }; + } + } + // if arg is an object return the object + else if(this._isObject(arg)) { + return arg; + } + + // default + return { + width: 0, + height: 0 + }; + }, + /* + * arg will be an array of numbers or + * an array of point objects + */ + _getPoints: function(arg) { + if(arg === undefined) { + return []; + } + + // an array of objects + if(this._isObject(arg[0])) { + return arg; + } + // an array of integers + else { + /* + * convert array of numbers into an array + * of objects containing x, y + */ + var arr = []; + for(var n = 0; n < arg.length; n += 2) { + arr.push({ + x: arg[n], + y: arg[n + 1] + }); + } + + return arr; + } + }, + /* + * arg can be an image object or image data + */ + _getImage: function(arg, callback) { + // if arg is null or undefined + if(!arg) { + callback(null); + } + + // if arg is already an image object + else if(this._isElement(arg)) { + callback(arg); + } + + // if arg is a string, then it's a data url + else if(this._isString(arg)) { + var imageObj = new Image(); + imageObj.onload = function() { + callback(imageObj); + } + imageObj.src = arg; + } + + //if arg is an object that contains the data property, it's an image object + else if(arg.data) { + var canvas = document.createElement('canvas'); + canvas.width = arg.width; + canvas.height = arg.height; + var context = canvas.getContext('2d'); + context.putImageData(arg, 0, 0); + var dataUrl = canvas.toDataURL(); + var imageObj = new Image(); + imageObj.onload = function() { + callback(imageObj); + } + imageObj.src = dataUrl; + } + + else { + callback(null); + } + } +}; + +/////////////////////////////////////////////////////////////////////// +// Canvas +/////////////////////////////////////////////////////////////////////// +/** + * Canvas wrapper constructor + * @constructor + * @param {Number} width + * @param {Number} height + */ +Kinetic.Canvas = function(width, height) { + this.element = document.createElement('canvas'); + this.context = this.element.getContext('2d'); + + // set dimensions + this.element.width = width; + this.element.height = height; +}; + +Kinetic.Canvas.prototype = { + /** + * clear canvas + * @name clear + * @methodOf Kinetic.Canvas.prototype + */ + clear: function() { + var context = this.getContext(); + var el = this.getElement(); + context.clearRect(0, 0, el.width, el.height); + }, + /** + * get element + * @name getElement + * @methodOf Kinetic.Canvas.prototype + */ + getElement: function() { + return this.element; + }, + /** + * get context + * @name getContext + * @methodOf Kinetic.Canvas.prototype + */ + getContext: function() { + return this.context; + }, + /** + * set width + * @name setWidth + * @methodOf Kinetic.Canvas.prototype + */ + setWidth: function(width) { + this.element.width = width; + }, + /** + * set height + * @name setHeight + * @methodOf Kinetic.Canvas.prototype + */ + setHeight: function(height) { + this.element.height = height; + }, + /** + * get width + * @name getWidth + * @methodOf Kinetic.Canvas.prototype + */ + getWidth: function() { + return this.element.width; + }, + /** + * get height + * @name getHeight + * @methodOf Kinetic.Canvas.prototype + */ + getHeight: function() { + return this.element.height; + }, + /** + * set size + * @name setSize + * @methodOf Kinetic.Canvas.prototype + */ + setSize: function(width, height) { + this.setWidth(width); + this.setHeight(height); + }, + /** + * strip away all functions that draw pixels onto the bitmap + * @name strip + * @methodOf Kinetic.Canvas.prototype + * @param {CanvasContext} context + */ + strip: function() { + var context = this.context; + context.stroke = function() { + }; + context.fill = function() { + }; + context.fillRect = function(x, y, width, height) { + context.rect(x, y, width, height); + }; + context.strokeRect = function(x, y, width, height) { + context.rect(x, y, width, height); + }; + context.drawImage = function() { + }; + context.fillText = function() { + }; + context.strokeText = function() { + }; + }, + /** + * toDataURL + */ + toDataURL: function(mimeType, quality) { + try { + // If this call fails (due to browser bug, like in Firefox 3.6), + // then revert to previous no-parameter image/png behavior + return this.element.toDataURL(mimeType, quality); + } + catch(e) { + return this.element.toDataURL(); + } + } +}; + +/////////////////////////////////////////////////////////////////////// +// Class +/////////////////////////////////////////////////////////////////////// +/* Simple JavaScript Inheritance +* By John Resig http://ejohn.org/ +* MIT Licensed. +*/ +// Inspired by base2 and Prototype +(function() { + var initializing = false; + // The base Class implementation (does nothing) + Kinetic.Class = function() { + }; + // Create a new Class that inherits from this class + Kinetic.Class.extend = function(prop) { + var _super = this.prototype; + + // Instantiate a base class (but only create the instance, + // don't run the init constructor) + initializing = true; + var prototype = new this(); + initializing = false; + + // Copy the properties over onto the new prototype + for(var name in prop) { + // Check if we're overwriting an existing function + prototype[name] = typeof prop[name] == "function" && typeof _super[name] == "function" ? (function(name, fn) { + return function() { + var tmp = this._super; + + // Add a new ._super() method that is the same method + // but on the super-class + this._super = _super[name]; + + // The method only need to be bound temporarily, so we + // remove it when we're done executing + var ret = fn.apply(this, arguments); + this._super = tmp; + + return ret; + }; + })(name, prop[name]) : prop[name]; + } + + // The dummy class constructor + function Class() { + // All construction is actually done in the init method + if(!initializing && this.init) + this.init.apply(this, arguments); + } + // Populate our constructed prototype object + Class.prototype = prototype; + + // Enforce the constructor to be what we expect + Class.prototype.constructor = Class; + + // And make this class extendable + Class.extend = arguments.callee; + + return Class; + }; +})(); +/////////////////////////////////////////////////////////////////////// +// Tween +/////////////////////////////////////////////////////////////////////// +/* +* The Tween class was ported from an Adobe Flash Tween library +* to JavaScript by Xaric. In the context of KineticJS, a Tween is +* an animation of a single Node property. A Transition is a set of +* multiple tweens +*/ +Kinetic.Tween = function(obj, propFunc, func, begin, finish, duration) { + this._listeners = []; + this.addListener(this); + this.obj = obj; + this.propFunc = propFunc; + this.begin = begin; + this._pos = begin; + this.setDuration(duration); + this.isPlaying = false; + this._change = 0; + this.prevTime = 0; + this.prevPos = 0; + this.looping = false; + this._time = 0; + this._position = 0; + this._startTime = 0; + this._finish = 0; + this.name = ''; + this.func = func; + this.setFinish(finish); +}; +/* + * Tween methods + */ +Kinetic.Tween.prototype = { + setTime: function(t) { + this.prevTime = this._time; + if(t > this.getDuration()) { + if(this.looping) { + this.rewind(t - this._duration); + this.update(); + this.broadcastMessage('onLooped', { + target: this, + type: 'onLooped' + }); + } + else { + this._time = this._duration; + this.update(); + this.stop(); + this.broadcastMessage('onFinished', { + target: this, + type: 'onFinished' + }); + } + } + else if(t < 0) { + this.rewind(); + this.update(); + } + else { + this._time = t; + this.update(); + } + }, + getTime: function() { + return this._time; + }, + setDuration: function(d) { + this._duration = (d === null || d <= 0) ? 100000 : d; + }, + getDuration: function() { + return this._duration; + }, + setPosition: function(p) { + this.prevPos = this._pos; + this.propFunc(p); + this._pos = p; + this.broadcastMessage('onChanged', { + target: this, + type: 'onChanged' + }); + }, + getPosition: function(t) { + if(t === undefined) { + t = this._time; + } + return this.func(t, this.begin, this._change, this._duration); + }, + setFinish: function(f) { + this._change = f - this.begin; + }, + getFinish: function() { + return this.begin + this._change; + }, + start: function() { + this.rewind(); + this.startEnterFrame(); + this.broadcastMessage('onStarted', { + target: this, + type: 'onStarted' + }); + }, + rewind: function(t) { + this.stop(); + this._time = (t === undefined) ? 0 : t; + this.fixTime(); + this.update(); + }, + fforward: function() { + this._time = this._duration; + this.fixTime(); + this.update(); + }, + update: function() { + this.setPosition(this.getPosition(this._time)); + }, + startEnterFrame: function() { + this.stopEnterFrame(); + this.isPlaying = true; + this.onEnterFrame(); + }, + onEnterFrame: function() { + if(this.isPlaying) { + this.nextFrame(); + } + }, + nextFrame: function() { + this.setTime((this.getTimer() - this._startTime) / 1000); + }, + stop: function() { + this.stopEnterFrame(); + this.broadcastMessage('onStopped', { + target: this, + type: 'onStopped' + }); + }, + stopEnterFrame: function() { + this.isPlaying = false; + }, + continueTo: function(finish, duration) { + this.begin = this._pos; + this.setFinish(finish); + if(this._duration !== undefined) { + this.setDuration(duration); + } + this.start(); + }, + resume: function() { + this.fixTime(); + this.startEnterFrame(); + this.broadcastMessage('onResumed', { + target: this, + type: 'onResumed' + }); + }, + yoyo: function() { + this.continueTo(this.begin, this._time); + }, + addListener: function(o) { + this.removeListener(o); + return this._listeners.push(o); + }, + removeListener: function(o) { + var a = this._listeners; + var i = a.length; + while(i--) { + if(a[i] == o) { + a.splice(i, 1); + return true; + } + } + return false; + }, + broadcastMessage: function() { + var arr = []; + for(var i = 0; i < arguments.length; i++) { + arr.push(arguments[i]); + } + var e = arr.shift(); + var a = this._listeners; + var l = a.length; + for(var i = 0; i < l; i++) { + if(a[i][e]) { + a[i][e].apply(a[i], arr); + } + } + }, + fixTime: function() { + this._startTime = this.getTimer() - this._time * 1000; + }, + getTimer: function() { + return new Date().getTime() - this._time; + } +}; + +Kinetic.Tweens = { + 'back-ease-in': function(t, b, c, d, a, p) { + var s = 1.70158; + return c * (t /= d) * t * ((s + 1) * t - s) + b; + }, + 'back-ease-out': function(t, b, c, d, a, p) { + var s = 1.70158; + return c * (( t = t / d - 1) * t * ((s + 1) * t + s) + 1) + b; + }, + 'back-ease-in-out': function(t, b, c, d, a, p) { + var s = 1.70158; + if((t /= d / 2) < 1) { + return c / 2 * (t * t * (((s *= (1.525)) + 1) * t - s)) + b; + } + return c / 2 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2) + b; + }, + 'elastic-ease-in': function(t, b, c, d, a, p) { + // added s = 0 + var s = 0; + if(t === 0) { + return b; + } + if((t /= d) == 1) { + return b + c; + } + if(!p) { + p = d * 0.3; + } + if(!a || a < Math.abs(c)) { + a = c; + s = p / 4; + } + else { + s = p / (2 * Math.PI) * Math.asin(c / a); + } + return -(a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p)) + b; + }, + 'elastic-ease-out': function(t, b, c, d, a, p) { + // added s = 0 + var s = 0; + if(t === 0) { + return b; + } + if((t /= d) == 1) { + return b + c; + } + if(!p) { + p = d * 0.3; + } + if(!a || a < Math.abs(c)) { + a = c; + s = p / 4; + } + else { + s = p / (2 * Math.PI) * Math.asin(c / a); + } + return (a * Math.pow(2, -10 * t) * Math.sin((t * d - s) * (2 * Math.PI) / p) + c + b); + }, + 'elastic-ease-in-out': function(t, b, c, d, a, p) { + // added s = 0 + var s = 0; + if(t === 0) { + return b; + } + if((t /= d / 2) == 2) { + return b + c; + } + if(!p) { + p = d * (0.3 * 1.5); + } + if(!a || a < Math.abs(c)) { + a = c; + s = p / 4; + } + else { + s = p / (2 * Math.PI) * Math.asin(c / a); + } + if(t < 1) { + return -0.5 * (a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p)) + b; + } + return a * Math.pow(2, -10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p) * 0.5 + c + b; + }, + 'bounce-ease-out': function(t, b, c, d) { + if((t /= d) < (1 / 2.75)) { + return c * (7.5625 * t * t) + b; + } + else if(t < (2 / 2.75)) { + return c * (7.5625 * (t -= (1.5 / 2.75)) * t + 0.75) + b; + } + else if(t < (2.5 / 2.75)) { + return c * (7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375) + b; + } + else { + return c * (7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375) + b; + } + }, + 'bounce-ease-in': function(t, b, c, d) { + return c - Kinetic.Tweens['bounce-ease-out'](d - t, 0, c, d) + b; + }, + 'bounce-ease-in-out': function(t, b, c, d) { + if(t < d / 2) { + return Kinetic.Tweens['bounce-ease-in'](t * 2, 0, c, d) * 0.5 + b; + } + else { + return Kinetic.Tweens['bounce-ease-out'](t * 2 - d, 0, c, d) * 0.5 + c * 0.5 + b; + } + }, + // duplicate + /* + strongEaseInOut: function(t, b, c, d) { + return c * (t /= d) * t * t * t * t + b; + }, + */ + 'ease-in': function(t, b, c, d) { + return c * (t /= d) * t + b; + }, + 'ease-out': function(t, b, c, d) { + return -c * (t /= d) * (t - 2) + b; + }, + 'ease-in-out': function(t, b, c, d) { + if((t /= d / 2) < 1) { + return c / 2 * t * t + b; + } + return -c / 2 * ((--t) * (t - 2) - 1) + b; + }, + 'strong-ease-in': function(t, b, c, d) { + return c * (t /= d) * t * t * t * t + b; + }, + 'strong-ease-out': function(t, b, c, d) { + return c * (( t = t / d - 1) * t * t * t * t + 1) + b; + }, + 'strong-ease-in-out': function(t, b, c, d) { + if((t /= d / 2) < 1) { + return c / 2 * t * t * t * t * t + b; + } + return c / 2 * ((t -= 2) * t * t * t * t + 2) + b; + }, + 'linear': function(t, b, c, d) { + return c * t / d + b; + } +}; + +/////////////////////////////////////////////////////////////////////// +// Transform +/////////////////////////////////////////////////////////////////////// +/* + * Last updated November 2011 + * By Simon Sarris + * www.simonsarris.com + * sarris@acm.org + * + * Free to use and distribute at will + * So long as you are nice to people, etc + */ + +/* + * The usage of this class was inspired by some of the work done by a forked + * project, KineticJS-Ext by Wappworks, which is based on Simon's Transform + * class. + */ + +Kinetic.Transform = function() { + this.m = [1, 0, 0, 1, 0, 0]; +} + +Kinetic.Transform.prototype = { + /** + * Apply translation + * @param {Number} x + * @param {Number} y + */ + translate: function(x, y) { + this.m[4] += this.m[0] * x + this.m[2] * y; + this.m[5] += this.m[1] * x + this.m[3] * y; + }, + /** + * Apply scale + * @param {Number} sx + * @param {Number} sy + */ + scale: function(sx, sy) { + this.m[0] *= sx; + this.m[1] *= sx; + this.m[2] *= sy; + this.m[3] *= sy; + }, + /** + * Apply rotation + * @param {Number} rad Angle in radians + */ + rotate: function(rad) { + var c = Math.cos(rad); + var s = Math.sin(rad); + var m11 = this.m[0] * c + this.m[2] * s; + var m12 = this.m[1] * c + this.m[3] * s; + var m21 = this.m[0] * -s + this.m[2] * c; + var m22 = this.m[1] * -s + this.m[3] * c; + this.m[0] = m11; + this.m[1] = m12; + this.m[2] = m21; + this.m[3] = m22; + }, + /** + * Returns the translation + * @returns {Object} 2D point(x, y) + */ + getTranslation: function() { + return { + x: this.m[4], + y: this.m[5] + }; + }, + /** + * Transform multiplication + * @param {Kinetic.Transform} matrix + */ + multiply: function(matrix) { + var m11 = this.m[0] * matrix.m[0] + this.m[2] * matrix.m[1]; + var m12 = this.m[1] * matrix.m[0] + this.m[3] * matrix.m[1]; + + var m21 = this.m[0] * matrix.m[2] + this.m[2] * matrix.m[3]; + var m22 = this.m[1] * matrix.m[2] + this.m[3] * matrix.m[3]; + + var dx = this.m[0] * matrix.m[4] + this.m[2] * matrix.m[5] + this.m[4]; + var dy = this.m[1] * matrix.m[4] + this.m[3] * matrix.m[5] + this.m[5]; + + this.m[0] = m11; + this.m[1] = m12; + this.m[2] = m21; + this.m[3] = m22; + this.m[4] = dx; + this.m[5] = dy; + }, + /** + * Invert the matrix + */ + invert: function() { + var d = 1 / (this.m[0] * this.m[3] - this.m[1] * this.m[2]); + var m0 = this.m[3] * d; + var m1 = -this.m[1] * d; + var m2 = -this.m[2] * d; + var m3 = this.m[0] * d; + var m4 = d * (this.m[2] * this.m[5] - this.m[3] * this.m[4]); + var m5 = d * (this.m[1] * this.m[4] - this.m[0] * this.m[5]); + this.m[0] = m0; + this.m[1] = m1; + this.m[2] = m2; + this.m[3] = m3; + this.m[4] = m4; + this.m[5] = m5; + }, + /** + * return matrix + */ + getMatrix: function() { + return this.m; + } +}; + +/////////////////////////////////////////////////////////////////////// +// Animation +/////////////////////////////////////////////////////////////////////// +Kinetic.Animation = function(config) { + if(!config) { + config = {}; + } + for(var key in config) { + this[key] = config[key]; + } + this.id = Kinetic.Animation.animIdCounter++; +}; +Kinetic.Animation.animations = []; +Kinetic.Animation.animIdCounter = 0; +Kinetic.Animation.animRunning = false; +Kinetic.Animation.frame = { + time: 0, + timeDiff: 0, + lastTime: new Date().getTime() +}; +Kinetic.Animation._addAnimation = function(anim) { + this.animations.push(anim); +}; +Kinetic.Animation._removeAnimation = function(anim) { + var id = anim.id; + var animations = this.animations; + for(var n = 0; n < animations.length; n++) { + if(animations[n].id === id) { + this.animations.splice(n, 1); + return false; + } + } +}; +Kinetic.Animation._runFrames = function() { + var nodes = {}; + /* + * loop through all animations and execute animation + * function. if the animation object has specified node, + * we can add the node to the nodes hash to eliminate + * drawing the same node multiple times. The node property + * can be the stage itself or a layer + */ + for(var n = 0; n < this.animations.length; n++) { + var anim = this.animations[n]; + if(anim.node && anim.node._id !== undefined) { + nodes[anim.node._id] = anim.node; + } + // if animation object has a function, execute it + if(anim.func) { + anim.func(this.frame); + } + } + + for(var key in nodes) { + nodes[key].draw(); + } +}; +Kinetic.Animation._updateFrameObject = function() { + var time = new Date().getTime(); + this.frame.timeDiff = time - this.frame.lastTime; + this.frame.lastTime = time; + this.frame.time += this.frame.timeDiff; +}; +Kinetic.Animation._animationLoop = function() { + if(this.animations.length > 0) { + this._updateFrameObject(); + this._runFrames(); + var that = this; + requestAnimFrame(function() { + that._animationLoop(); + }); + } + else { + this.animRunning = false; + this.frame.lastTime = 0; + } +}; +Kinetic.Animation._handleAnimation = function() { + var that = this; + if(!this.animRunning) { + this.animRunning = true; + that._animationLoop(); + } + else { + this.frame.lastTime = 0; + } +}; +requestAnimFrame = (function(callback) { + return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || + function(callback) { + window.setTimeout(callback, 1000 / 60); + }; +})(); + +/////////////////////////////////////////////////////////////////////// +// Node +/////////////////////////////////////////////////////////////////////// +/** + * Node constructor.  Nodes are entities that can be transformed, layered, + * and have events bound to them. They are the building blocks of a KineticJS + * application + * @constructor + * @param {Object} config + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.alpha] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] + * @param {Number} [config.scale.x] + * @param {Number} [config.scale.y] + * @param {Number} [config.rotation] rotation in radians + * @param {Number} [config.rotationDeg] rotation in degrees + * @param {Object} [config.offset] offsets default position point and rotation point + * @param {Number} [config.offset.x] + * @param {Number} [config.offset.y] + * @param {Boolean} [config.draggable] + * @param {String} [config.dragConstraint] can be vertical, horizontal, or none. The default + * is none + * @param {Object} [config.dragBounds] + * @param {Number} [config.dragBounds.top] + * @param {Number} [config.dragBounds.right] + * @param {Number} [config.dragBounds.bottom] + * @param {Number} [config.dragBounds.left] + * @param {Number} [config.dragThrottle] drag and drop throttling in draws per second. The + * default is 80 draws per second. Increasing the dragThrottle will increase the number of + * draws and may result in slow drag performance in some browsers. Reducing the dragThrottle + * will improve drag performance but may make the drag motion jumpy + */ +Kinetic.Node = Kinetic.Class.extend({ + init: function(config) { + this.defaultNodeAttrs = { + visible: true, + listening: true, + name: undefined, + alpha: 1, + x: 0, + y: 0, + scale: { + x: 1, + y: 1 + }, + rotation: 0, + offset: { + x: 0, + y: 0 + }, + dragConstraint: 'none', + dragBounds: {}, + draggable: false, + dragThrottle: 80 + }; + + this.setDefaultAttrs(this.defaultNodeAttrs); + this.eventListeners = {}; + this.lastDragTime = 0; + this.transAnim = new Kinetic.Animation(); + this.setAttrs(config); + + // bind events + this.on('draggableChange.kinetic', function() { + this._onDraggableChange(); + }); + var that = this; + this.on('idChange.kinetic', function(evt) { + var stage = that.getStage(); + if(stage) { + stage._removeId(evt.oldVal); + stage._addId(that); + } + }); + this.on('nameChange.kinetic', function(evt) { + var stage = that.getStage(); + if(stage) { + stage._removeName(evt.oldVal, that._id); + stage._addName(that); + } + }); + + this._onDraggableChange(); + }, + /** + * bind events to the node. KineticJS supports mouseover, mousemove, + * mouseout, mousedown, mouseup, click, dblclick, touchstart, touchmove, + * touchend, tap, dbltap, dragstart, dragmove, and dragend. Pass in a string + * of event types delimmited by a space to bind multiple events at once + * such as 'mousedown mouseup mousemove'. include a namespace to bind an + * event by name such as 'click.foobar'. + * @name on + * @methodOf Kinetic.Node.prototype + * @param {String} typesStr + * @param {Function} handler + */ + on: function(typesStr, handler) { + var types = typesStr.split(' '); + /* + * loop through types and attach event listeners to + * each one. eg. 'click mouseover.namespace mouseout' + * will create three event bindings + */ + for(var n = 0; n < types.length; n++) { + var type = types[n]; + var event = type; + var parts = event.split('.'); + var baseEvent = parts[0]; + var name = parts.length > 1 ? parts[1] : ''; + + if(!this.eventListeners[baseEvent]) { + this.eventListeners[baseEvent] = []; + } + + this.eventListeners[baseEvent].push({ + name: name, + handler: handler + }); + } + }, + /** + * remove event bindings from the node. Pass in a string of + * event types delimmited by a space to remove multiple event + * bindings at once such as 'mousedown mouseup mousemove'. + * include a namespace to remove an event binding by name + * such as 'click.foobar'. + * @name off + * @methodOf Kinetic.Node.prototype + * @param {String} typesStr + */ + off: function(typesStr) { + var types = typesStr.split(' '); + + for(var n = 0; n < types.length; n++) { + var type = types[n]; + //var event = (type.indexOf('touch') === -1) ? 'on' + type : type; + var event = type; + var parts = event.split('.'); + var baseEvent = parts[0]; + + if(this.eventListeners[baseEvent] && parts.length > 1) { + var name = parts[1]; + + for(var i = 0; i < this.eventListeners[baseEvent].length; i++) { + if(this.eventListeners[baseEvent][i].name === name) { + this.eventListeners[baseEvent].splice(i, 1); + if(this.eventListeners[baseEvent].length === 0) { + delete this.eventListeners[baseEvent]; + break; + } + i--; + } + } + } + else { + delete this.eventListeners[baseEvent]; + } + } + }, + /** + * get attrs + * @name getAttrs + * @methodOf Kinetic.Node.prototype + */ + getAttrs: function() { + return this.attrs; + }, + /** + * set default attrs. This method should only be used if + * you're creating a custom node + * @name setDefaultAttrs + * @methodOf Kinetic.Node.prototype + * @param {Object} confic + */ + setDefaultAttrs: function(config) { + // create attrs object if undefined + if(this.attrs === undefined) { + this.attrs = {}; + } + + if(config) { + for(var key in config) { + /* + * only set the attr if it's undefined in case + * a developer writes a custom class that extends + * a Kinetic Class such that their default property + * isn't overwritten by the Kinetic Class default + * property + */ + if(this.attrs[key] === undefined) { + this.attrs[key] = config[key]; + } + } + } + }, + /** + * set attrs + * @name setAttrs + * @methodOf Kinetic.Node.prototype + * @param {Object} config + */ + setAttrs: function(config) { + var type = Kinetic.Type; + var that = this; + // set properties from config + if(config !== undefined) { + function setAttrs(obj, c, level) { + for(var key in c) { + var val = c[key]; + var oldVal = obj[key]; + + /* + * only fire change event for root + * level attrs + */ + if(level === 0) { + that._fireBeforeChangeEvent(key, oldVal, val); + } + + // if obj doesn't have the val property, then create it + if(obj[key] === undefined && val !== undefined) { + obj[key] = {}; + } + + /* + * if property is a pure object (no methods), then add an empty object + * to the node and then traverse + */ + if(type._isObject(val) && !type._isArray(val) && !type._isElement(val) && !type._hasMethods(val)) { + /* + * since some properties can be strings or objects, e.g. + * fill, we need to first check that obj is an object + * before setting properties. If it's not an object, + * overwrite obj with an object literal + */ + if(!Kinetic.Type._isObject(obj[key])) { + obj[key] = {}; + } + + setAttrs(obj[key], val, level + 1); + } + /* + * add all other object types to attrs object + */ + else { + // handle special keys + switch (key) { + case 'rotationDeg': + that._setAttr(obj, 'rotation', c[key] * Math.PI / 180); + // override key for change event + key = 'rotation'; + break; + /* + * includes: + * - node offset + * - fill pattern offset + * - shadow offset + */ + case 'offset': + var pos = type._getXY(val); + that._setAttr(obj[key], 'x', pos.x); + that._setAttr(obj[key], 'y', pos.y); + break; + case 'scale': + var pos = type._getXY(val); + that._setAttr(obj[key], 'x', pos.x); + that._setAttr(obj[key], 'y', pos.y); + break; + case 'points': + that._setAttr(obj, key, type._getPoints(val)); + break; + case 'crop': + var pos = type._getXY(val); + var size = type._getSize(val); + that._setAttr(obj[key], 'x', pos.x); + that._setAttr(obj[key], 'y', pos.y); + that._setAttr(obj[key], 'width', size.width); + that._setAttr(obj[key], 'height', size.height); + break; + default: + that._setAttr(obj, key, val); + break; + } + } + /* + * only fire change event for root + * level attrs + */ + if(level === 0) { + that._fireChangeEvent(key, oldVal, val); + } + } + } + setAttrs(this.attrs, config, 0); + } + }, + /** + * determine if shape is visible or not. Shape is visible only + * if it's visible and all of its ancestors are visible. If an ancestor + * is invisible, this means that the shape is also invisible + * @name isVisible + * @methodOf Kinetic.Node.prototype + */ + isVisible: function() { + if(this.attrs.visible && this.getParent() && !this.getParent().isVisible()) { + return false; + } + return this.attrs.visible; + }, + /** + * show node + * @name show + * @methodOf Kinetic.Node.prototype + */ + show: function() { + this.setAttrs({ + visible: true + }); + }, + /** + * hide node. Hidden nodes are no longer detectable + * @name hide + * @methodOf Kinetic.Node.prototype + */ + hide: function() { + this.setAttrs({ + visible: false + }); + }, + /** + * get zIndex + * @name getZIndex + * @methodOf Kinetic.Node.prototype + */ + getZIndex: function() { + return this.index; + }, + /** + * get absolute z-index which takes into account sibling + * and parent indices + * @name getAbsoluteZIndex + * @methodOf Kinetic.Node.prototype + */ + getAbsoluteZIndex: function() { + var level = this.getLevel(); + var stage = this.getStage(); + var that = this; + var index = 0; + function addChildren(children) { + var nodes = []; + for(var n = 0; n < children.length; n++) { + var child = children[n]; + index++; + + if(child.nodeType !== 'Shape') { + nodes = nodes.concat(child.getChildren()); + } + + if(child._id === that._id) { + n = children.length; + } + } + + if(nodes.length > 0 && nodes[0].getLevel() <= level) { + addChildren(nodes); + } + } + if(that.nodeType !== 'Stage') { + addChildren(that.getStage().getChildren()); + } + + return index; + }, + /** + * get node level in node tree + * @name getLevel + * @methodOf Kinetic.Node.prototype + */ + getLevel: function() { + var level = 0; + var parent = this.parent; + while(parent) { + level++; + parent = parent.parent; + } + return level; + }, + /** + * set node position + * @name setPosition + * @methodOf Kinetic.Node.prototype + * @param {Number} x + * @param {Number} y + */ + setPosition: function() { + var pos = Kinetic.Type._getXY(Array.prototype.slice.call(arguments)); + this.setAttrs(pos); + }, + /** + * get node position relative to container + * @name getPosition + * @methodOf Kinetic.Node.prototype + */ + getPosition: function() { + return { + x: this.attrs.x, + y: this.attrs.y + }; + }, + /** + * get absolute position + * @name getAbsolutePosition + * @methodOf Kinetic.Node.prototype + */ + getAbsolutePosition: function() { + var trans = this.getAbsoluteTransform(); + var o = this.getOffset(); + trans.translate(o.x, o.y); + return trans.getTranslation(); + }, + /** + * set absolute position + * @name setAbsolutePosition + * @methodOf Kinetic.Node.prototype + * @param {Object} pos object containing an x and + * y property + */ + setAbsolutePosition: function() { + var pos = Kinetic.Type._getXY(Array.prototype.slice.call(arguments)); + var trans = this._clearTransform(); + // don't clear translation + this.attrs.x = trans.x; + this.attrs.y = trans.y; + delete trans.x; + delete trans.y; + + // unravel transform + var it = this.getAbsoluteTransform(); + + it.invert(); + it.translate(pos.x, pos.y); + pos = { + x: this.attrs.x + it.getTranslation().x, + y: this.attrs.y + it.getTranslation().y + }; + + this.setPosition(pos.x, pos.y); + this._setTransform(trans); + }, + /** + * move node by an amount + * @name move + * @methodOf Kinetic.Node.prototype + * @param {Number} x + * @param {Number} y + */ + move: function() { + var pos = Kinetic.Type._getXY(Array.prototype.slice.call(arguments)); + + var x = this.getX(); + var y = this.getY(); + + if(pos.x !== undefined) { + x += pos.x; + } + + if(pos.y !== undefined) { + y += pos.y; + } + + this.setAttrs({ + x: x, + y: y + }); + }, + /** + * get rotation in degrees + * @name getRotationDeg + * @methodOf Kinetic.Node.prototype + */ + getRotationDeg: function() { + return this.attrs.rotation * 180 / Math.PI; + }, + /** + * rotate node by an amount in radians + * @name rotate + * @methodOf Kinetic.Node.prototype + * @param {Number} theta + */ + rotate: function(theta) { + this.setAttrs({ + rotation: this.getRotation() + theta + }); + }, + /** + * rotate node by an amount in degrees + * @name rotateDeg + * @methodOf Kinetic.Node.prototype + * @param {Number} deg + */ + rotateDeg: function(deg) { + this.setAttrs({ + rotation: this.getRotation() + (deg * Math.PI / 180) + }); + }, + /** + * move node to the top of its siblings + * @name moveToTop + * @methodOf Kinetic.Node.prototype + */ + moveToTop: function() { + var index = this.index; + this.parent.children.splice(index, 1); + this.parent.children.push(this); + this.parent._setChildrenIndices(); + }, + /** + * move node up + * @name moveUp + * @methodOf Kinetic.Node.prototype + */ + moveUp: function() { + var index = this.index; + this.parent.children.splice(index, 1); + this.parent.children.splice(index + 1, 0, this); + this.parent._setChildrenIndices(); + }, + /** + * move node down + * @name moveDown + * @methodOf Kinetic.Node.prototype + */ + moveDown: function() { + var index = this.index; + if(index > 0) { + this.parent.children.splice(index, 1); + this.parent.children.splice(index - 1, 0, this); + this.parent._setChildrenIndices(); + } + }, + /** + * move node to the bottom of its siblings + * @name moveToBottom + * @methodOf Kinetic.Node.prototype + */ + moveToBottom: function() { + var index = this.index; + this.parent.children.splice(index, 1); + this.parent.children.unshift(this); + this.parent._setChildrenIndices(); + }, + /** + * set zIndex + * @name setZIndex + * @methodOf Kinetic.Node.prototype + * @param {Integer} zIndex + */ + setZIndex: function(zIndex) { + var index = this.index; + this.parent.children.splice(index, 1); + this.parent.children.splice(zIndex, 0, this); + this.parent._setChildrenIndices(); + }, + /** + * get absolute alpha + * @name getAbsoluteAlpha + * @methodOf Kinetic.Node.prototype + */ + getAbsoluteAlpha: function() { + var absAlpha = 1; + var node = this; + // traverse upwards + while(node.nodeType !== 'Stage') { + absAlpha *= node.attrs.alpha; + node = node.parent; + } + return absAlpha; + }, + /** + * determine if node is currently in drag and drop mode + * @name isDragging + * @methodOf Kinetic.Node.prototype + */ + isDragging: function() { + var go = Kinetic.Global; + return go.drag.node !== undefined && go.drag.node._id === this._id && go.drag.moving; + }, + /** + * move node to another container + * @name moveTo + * @methodOf Kinetic.Node.prototype + * @param {Container} newContainer + */ + moveTo: function(newContainer) { + var parent = this.parent; + // remove from parent's children + parent.children.splice(this.index, 1); + parent._setChildrenIndices(); + + // add to new parent + newContainer.children.push(this); + this.index = newContainer.children.length - 1; + this.parent = newContainer; + newContainer._setChildrenIndices(); + }, + /** + * get parent container + * @name getParent + * @methodOf Kinetic.Node.prototype + */ + getParent: function() { + return this.parent; + }, + /** + * get layer that contains the node + * @name getLayer + * @methodOf Kinetic.Node.prototype + */ + getLayer: function() { + if(this.nodeType === 'Layer') { + return this; + } + else { + return this.getParent().getLayer(); + } + }, + /** + * get stage that contains the node + * @name getStage + * @methodOf Kinetic.Node.prototype + */ + getStage: function() { + if(this.nodeType !== 'Stage' && this.getParent()) { + return this.getParent().getStage(); + } + else if(this.nodeType === 'Stage') { + return this; + } + else { + return undefined; + } + }, + /** + * simulate event + * @name simulate + * @methodOf Kinetic.Node.prototype + * @param {String} eventType + */ + simulate: function(eventType) { + this._handleEvent(eventType, {}); + }, + /** + * transition node to another state. Any property that can accept a real + * number can be transitioned, including x, y, rotation, alpha, strokeWidth, + * radius, scale.x, scale.y, offset.x, offset.y, etc. + * @name transitionTo + * @methodOf Kinetic.Node.prototype + * @param {Object} config + * @config {Number} duration duration that the transition runs in seconds + * @config {String} [easing] easing function. can be linear, ease-in, ease-out, ease-in-out, + * back-ease-in, back-ease-out, back-ease-in-out, elastic-ease-in, elastic-ease-out, + * elastic-ease-in-out, bounce-ease-out, bounce-ease-in, bounce-ease-in-out, + * strong-ease-in, strong-ease-out, or strong-ease-in-out + * linear is the default + * @config {Function} [callback] callback function to be executed when + * transition completes + */ + transitionTo: function(config) { + var a = Kinetic.Animation; + + /* + * clear transition if one is currently running for this + * node + */ + a._removeAnimation(this.transAnim); + + /* + * create new transition + */ + var node = this.nodeType === 'Stage' ? this : this.getLayer(); + var that = this; + var trans = new Kinetic.Transition(this, config); + + this.transAnim.func = function() { + trans._onEnterFrame(); + }; + this.transAnim.node = node; + /* + * adding the animation with the addAnimation + * method auto generates an id + */ + a._addAnimation(this.transAnim); + + // subscribe to onFinished for first tween + trans.onFinished = function() { + // remove animation + a._removeAnimation(that.transAnim); + that.transAnim.node.draw(); + + // callback + if(config.callback) { + config.callback(); + } + }; + // auto start + trans.start(); + a._handleAnimation(); + return trans; + }, + /** + * get absolute transform of the node which takes into + * account its parent transforms + * @name getAbsoluteTransform + * @methodOf Kinetic.Node.prototype + */ + getAbsoluteTransform: function() { + // absolute transform + var am = new Kinetic.Transform(); + + var family = []; + var parent = this.parent; + + family.unshift(this); + while(parent) { + family.unshift(parent); + parent = parent.parent; + } + + for(var n = 0; n < family.length; n++) { + var node = family[n]; + var m = node.getTransform(); + am.multiply(m); + } + + return am; + }, + /** + * get transform of the node + * @name getTransform + * @methodOf Kinetic.Node.prototype + */ + getTransform: function() { + var m = new Kinetic.Transform(); + + if(this.attrs.x !== 0 || this.attrs.y !== 0) { + m.translate(this.attrs.x, this.attrs.y); + } + if(this.attrs.rotation !== 0) { + m.rotate(this.attrs.rotation); + } + if(this.attrs.scale.x !== 1 || this.attrs.scale.y !== 1) { + m.scale(this.attrs.scale.x, this.attrs.scale.y); + } + if(this.attrs.offset && (this.attrs.offset.x !== 0 || this.attrs.offset.y !== 0)) { + m.translate(-1 * this.attrs.offset.x, -1 * this.attrs.offset.y); + } + + return m; + }, + /** + * clone node + * @name clone + * @methodOf Kinetic.Node.prototype + * @param {Object} attrs override attrs + */ + clone: function(obj) { + // instantiate new node + var classType = this.shapeType || this.nodeType; + var node = new Kinetic[classType](this.attrs); + + /* + * copy over user listeners + */ + for(var key in this.eventListeners) { + var allListeners = this.eventListeners[key]; + for(var n = 0; n < allListeners.length; n++) { + var listener = allListeners[n]; + /* + * don't include kinetic namespaced listeners because + * these are generated by the constructors + */ + if(listener.name.indexOf('kinetic') < 0) { + // if listeners array doesn't exist, then create it + if(!node.eventListeners[key]) { + node.eventListeners[key] = []; + } + node.eventListeners[key].push(listener); + } + } + } + + // apply attr overrides + node.setAttrs(obj); + return node; + }, + /** + * save image data + * @name saveImageData + * @methodOf Kinetic.Node.prototype + */ + saveImageData: function(width, height) { + try { + var canvas; + if(width && height) { + canvas = new Kinetic.Canvas(width, height); + } + else { + var stage = this.getStage(); + canvas = stage.bufferCanvas; + } + + var context = canvas.getContext(); + canvas.clear(); + this._draw(canvas); + var imageData = context.getImageData(0, 0, canvas.getWidth(), canvas.getHeight()); + this.imageData = imageData; + } + catch(e) { + Kinetic.Global.warn('Image data could not saved because canvas is dirty.'); + } + }, + /** + * clear image data + * @name clearImageData + * @methodOf Kinetic.Node.prototype + */ + clearImageData: function() { + delete this.imageData; + }, + /** + * get image data + * @name getImageData + * @methodOf Kinetic.Node.prototype + */ + getImageData: function() { + return this.imageData; + }, + /** + * Creates a composite data URL. If MIME type is not + * specified, then "image/png" will result. For "image/jpeg", specify a quality + * level as quality (range 0.0 - 1.0) + * @name toDataURL + * @methodOf Kinetic.Node.prototype + * @param {Object} config + * @param {String} [config.mimeType] mime type. can be "image/png" or "image/jpeg". + * "image/png" is the default + * @param {Number} [config.width] data url image width + * @param {Number} [config.height] data url image height + * @param {Number} [config.quality] jpeg quality. If using an "image/jpeg" mimeType, + * you can specify the quality from 0 to 1, where 0 is very poor quality and 1 + * is very high quality + */ + toDataURL: function(config) { + var mimeType = config && config.mimeType ? config.mimeType : null; + var quality = config && config.quality ? config.quality : null; + var canvas; + if(config && config.width && config.height) { + canvas = new Kinetic.Canvas(config.width, config.height); + } + else { + canvas = this.getStage().bufferCanvas; + } + + var context = canvas.getContext(); + canvas.clear(); + this._draw(canvas); + return canvas.toDataURL(mimeType, quality); + }, + /** + * converts node into an image. Since the toImage + * method is asynchronous, a callback is required + * @name toImage + * @methodOf Kinetic.Stage.prototype + * @param {Object} config + * @param {Function} callback since the toImage() method is asynchonrous, the + * resulting image object is passed into the callback function + * @param {String} [config.mimeType] mime type. can be "image/png" or "image/jpeg". + * "image/png" is the default + * @param {Number} [config.width] data url image width + * @param {Number} [config.height] data url image height + * @param {Number} [config.quality] jpeg quality. If using an "image/jpeg" mimeType, + * you can specify the quality from 0 to 1, where 0 is very poor quality and 1 + * is very high quality + */ + toImage: function(config) { + Kinetic.Type._getImage(this.toDataURL(config), function(img) { + config.callback(img); + }); + }, + _clearTransform: function() { + var trans = { + x: this.attrs.x, + y: this.attrs.y, + rotation: this.attrs.rotation, + scale: { + x: this.attrs.scale.x, + y: this.attrs.scale.y + }, + offset: { + x: this.attrs.offset.x, + y: this.attrs.offset.y + } + }; + + this.attrs.x = 0; + this.attrs.y = 0; + this.attrs.rotation = 0; + this.attrs.scale = { + x: 1, + y: 1 + }; + this.attrs.offset = { + x: 0, + y: 0 + }; + + return trans; + }, + _setTransform: function(trans) { + for(var key in trans) { + this.attrs[key] = trans[key]; + } + }, + _setImageData: function(imageData) { + if(imageData && imageData.data) { + this.imageData = imageData; + } + }, + _fireBeforeChangeEvent: function(attr, oldVal, newVal) { + this._handleEvent('before' + attr.toUpperCase() + 'Change', { + oldVal: oldVal, + newVal: newVal + }); + }, + _fireChangeEvent: function(attr, oldVal, newVal) { + this._handleEvent(attr + 'Change', { + oldVal: oldVal, + newVal: newVal + }); + }, + _setAttr: function(obj, attr, val) { + if(val !== undefined) { + if(obj === undefined) { + obj = {}; + } + obj[attr] = val; + } + }, + _listenDrag: function() { + this._dragCleanup(); + var go = Kinetic.Global; + var that = this; + this.on('mousedown.kinetic touchstart.kinetic', function(evt) { + that._initDrag(); + }); + }, + _initDrag: function() { + var go = Kinetic.Global; + var stage = this.getStage(); + var pos = stage.getUserPosition(); + + if(pos) { + var m = this.getTransform().getTranslation(); + var am = this.getAbsoluteTransform().getTranslation(); + var ap = this.getAbsolutePosition(); + go.drag.node = this; + go.drag.offset.x = pos.x - ap.x; + go.drag.offset.y = pos.y - ap.y; + } + }, + _onDraggableChange: function() { + if(this.attrs.draggable) { + this._listenDrag(); + } + else { + // remove event listeners + this._dragCleanup(); + + /* + * force drag and drop to end + * if this node is currently in + * drag and drop mode + */ + var stage = this.getStage(); + var go = Kinetic.Global; + if(stage && go.drag.node && go.drag.node._id === this._id) { + stage._endDrag(); + } + } + }, + /** + * remove drag and drop event listener + */ + _dragCleanup: function() { + this.off('mousedown.kinetic'); + this.off('touchstart.kinetic'); + }, + /** + * handle node event + */ + _handleEvent: function(eventType, evt) { + if(this.nodeType === 'Shape') { + evt.shape = this; + } + + var stage = this.getStage(); + var mover = stage ? stage.mouseoverShape : null; + var mout = stage ? stage.mouseoutShape : null; + var el = this.eventListeners; + var okayToRun = true; + + /* + * determine if event handler should be skipped by comparing + * parent nodes + */ + if(eventType === 'mouseover' && mout && mout._id === this._id) { + okayToRun = false; + } + else if(eventType === 'mouseout' && mover && mover._id === this._id) { + okayToRun = false; + } + + if(okayToRun) { + if(el[eventType]) { + var events = el[eventType]; + for(var i = 0; i < events.length; i++) { + events[i].handler.apply(this, [evt]); + } + } + + if(stage && mover && mout) { + stage.mouseoverShape = mover.parent; + stage.mouseoutShape = mout.parent; + } + + // simulate event bubbling + if(Kinetic.Global.BUBBLE_WHITELIST.indexOf(eventType) >= 0 && !evt.cancelBubble && this.parent) { + this._handleEvent.call(this.parent, eventType, evt); + } + } + } +}); + +// add getter and setter methods +Kinetic.Node.addSetters = function(constructor, arr) { + for(var n = 0; n < arr.length; n++) { + var attr = arr[n]; + this._addSetter(constructor, attr); + } +}; +Kinetic.Node.addGetters = function(constructor, arr) { + for(var n = 0; n < arr.length; n++) { + var attr = arr[n]; + this._addGetter(constructor, attr); + } +}; +Kinetic.Node.addGettersSetters = function(constructor, arr) { + this.addSetters(constructor, arr); + this.addGetters(constructor, arr); +}; +Kinetic.Node._addSetter = function(constructor, attr) { + var that = this; + var method = 'set' + attr.charAt(0).toUpperCase() + attr.slice(1); + constructor.prototype[method] = function() { + if(arguments.length == 1) { + arg = arguments[0]; + } + else { + arg = Array.prototype.slice.call(arguments); + } + var obj = {}; + obj[attr] = arg; + this.setAttrs(obj); + }; +}; +Kinetic.Node._addGetter = function(constructor, attr) { + var that = this; + var method = 'get' + attr.charAt(0).toUpperCase() + attr.slice(1); + constructor.prototype[method] = function(arg) { + return this.attrs[attr]; + }; +}; +// add getters setters +Kinetic.Node.addGettersSetters(Kinetic.Node, ['x', 'y', 'scale', 'detectionType', 'rotation', 'alpha', 'name', 'id', 'offset', 'draggable', 'dragConstraint', 'dragBounds', 'listening', 'dragThrottle']); +Kinetic.Node.addSetters(Kinetic.Node, ['rotationDeg']); + +/** + * set node x position + * @name setX + * @methodOf Kinetic.Node.prototype + * @param {Number} x + */ + +/** + * set node y position + * @name setY + * @methodOf Kinetic.Node.prototype + * @param {Number} y + */ + +/** + * set detection type + * @name setDetectionType + * @methodOf Kinetic.Node.prototype + * @param {String} type can be path or pixel + */ + +/** + * set node rotation in radians + * @name setRotation + * @methodOf Kinetic.Node.prototype + * @param {Number} theta + */ + +/** + * set alpha. Alpha values range from 0 to 1. + * A node with an alpha of 0 is fully transparent, and a node + * with an alpha of 1 is fully opaque + * @name setAlpha + * @methodOf Kinetic.Node.prototype + * @param {Object} alpha + */ + +/** + * set drag throttle + * @name setDragThrottle + * @methodOf Kinetic.Node.prototype + * @param {Number} dragThrottle + */ + +/** + * set draggable + * @name setDraggable + * @methodOf Kinetic.Node.prototype + * @param {String} draggable + */ + +/** + * set drag constraint. + * @name setDragConstraint + * @methodOf Kinetic.Node.prototype + * @param {String} constraint can be vertical, horizontal, or none + */ + +/** + * set drag bounds. + * @name setDragBounds + * @methodOf Kinetic.Node.prototype + * @param {Object} bounds + * @config {Number} [left] left bounds position + * @config {Number} [top] top bounds position + * @config {Number} [right] right bounds position + * @config {Number} [bottom] bottom bounds position + */ + +/** + * listen or don't listen to events + * @name setListening + * @methodOf Kinetic.Node.prototype + * @param {Boolean} listening + */ + +/** + * set node rotation in degrees + * @name setRotationDeg + * @methodOf Kinetic.Node.prototype + * @param {Number} deg + */ + +/** + * set offset. A node's offset defines the positition and rotation point + * @name setOffset + * @methodOf Kinetic.Node.prototype + * @param {Number} x + * @param {Number} y + */ + +/** + * set node scale. + * @name setScale + * @param {Number} x + * @param {Number} y + * @methodOf Kinetic.Node.prototype + */ + +/** + * get scale + * @name getScale + * @methodOf Kinetic.Node.prototype + */ + +/** + * get node x position + * @name getX + * @methodOf Kinetic.Node.prototype + */ + +/** + * get node y position + * @name getY + * @methodOf Kinetic.Node.prototype + */ + +/** + * get detection type. Can be path or pixel + * @name getDetectionType + * @methodOf Kinetic.Node.prototype + */ + +/** + * get rotation in radians + * @name getRotation + * @methodOf Kinetic.Node.prototype + */ + +/** + * get alpha. + * @name getAlpha + * @methodOf Kinetic.Node.prototype + */ + +/** + * get drag throttle. + * @name getDragThrottle + * @methodOf Kinetic.Node.prototype + */ + +/** + * get name + * @name getName + * @methodOf Kinetic.Node.prototype + */ + +/** + * get id + * @name getId + * @methodOf Kinetic.Node.prototype + */ + +/** + * get offset + * @name getOffset + * @methodOf Kinetic.Node.prototype + */ + +/** + * get draggable + * @name getDraggable + * @methodOf Kinetic.Node.prototype + */ + +/** + * get drag constraint + * @name getDragConstraint + * @methodOf Kinetic.Node.prototype + */ + +/** + * get drag bounds + * @name getDragBounds + * @methodOf Kinetic.Node.prototype + */ + +/** + * determine if listening to events or not + * @name getListening + * @methodOf Kinetic.Node.prototype + */ +/////////////////////////////////////////////////////////////////////// +// Container +/////////////////////////////////////////////////////////////////////// +/** + * Container constructor.  Containers are used to contain nodes or other containers + * @constructor + * @augments Kinetic.Node + * @param {Object} config + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.alpha] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] + * @param {Number} [config.scale.x] + * @param {Number} [config.scale.y] + * @param {Number} [config.rotation] rotation in radians + * @param {Number} [config.rotationDeg] rotation in degrees + * @param {Object} [config.offset] offsets default position point and rotation point + * @param {Number} [config.offset.x] + * @param {Number} [config.offset.y] + * @param {Boolean} [config.draggable] + * @param {String} [config.dragConstraint] can be vertical, horizontal, or none. The default + * is none + * @param {Object} [config.dragBounds] + * @param {Number} [config.dragBounds.top] + * @param {Number} [config.dragBounds.right] + * @param {Number} [config.dragBounds.bottom] + * @param {Number} [config.dragBounds.left] + */ +Kinetic.Container = Kinetic.Node.extend({ + init: function(config) { + this.children = []; + this._super(config); + }, + /** + * get children + * @name getChildren + * @methodOf Kinetic.Container.prototype + */ + getChildren: function() { + return this.children; + }, + /** + * remove all children + * @name removeChildren + * @methodOf Kinetic.Container.prototype + */ + removeChildren: function() { + while(this.children.length > 0) { + this.remove(this.children[0]); + } + }, + /** + * add node to container + * @name add + * @methodOf Kinetic.Container.prototype + * @param {Node} child + */ + add: function(child) { + child._id = Kinetic.Global.idCounter++; + child.index = this.children.length; + child.parent = this; + + this.children.push(child); + var stage = child.getStage(); + + if(!stage) { + Kinetic.Global._addTempNode(child); + } + else { + stage._addId(child); + stage._addName(child); + + /* + * pull in other nodes that are now linked + * to a stage + */ + var go = Kinetic.Global; + go._pullNodes(stage); + } + + // do extra stuff if needed + if(this._add !== undefined) { + this._add(child); + } + + // chainable + return this; + }, + /** + * remove child from container + * @name remove + * @methodOf Kinetic.Container.prototype + * @param {Node} child + */ + remove: function(child) { + if(child && child.index !== undefined && this.children[child.index]._id == child._id) { + var stage = this.getStage(); + /* + * remove event listeners and references to the node + * from the ids and names hashes + */ + if(stage) { + stage._removeId(child.getId()); + stage._removeName(child.getName(), child._id); + } + + Kinetic.Global._removeTempNode(child); + this.children.splice(child.index, 1); + this._setChildrenIndices(); + + // remove children + while(child.children && child.children.length > 0) { + child.remove(child.children[0]); + } + + // do extra stuff if needed + if(this._remove !== undefined) { + this._remove(child); + } + } + + // chainable + return this; + }, + /** + * return an array of nodes that match the selector. Use '#' for id selections + * and '.' for name selections + * ex: + * var node = stage.get('#foo'); // selects node with id foo + * var nodes = layer.get('.bar'); // selects nodes with name bar inside layer + * @name get + * @methodOf Kinetic.Container.prototype + * @param {String} selector + */ + get: function(selector) { + var stage = this.getStage(); + var arr; + var key = selector.slice(1); + if(selector.charAt(0) === '#') { + arr = stage.ids[key] !== undefined ? [stage.ids[key]] : []; + } + else if(selector.charAt(0) === '.') { + arr = stage.names[key] !== undefined ? stage.names[key] : []; + } + else if(selector === 'Shape' || selector === 'Group' || selector === 'Layer') { + return this._getNodes(selector); + } + else { + return false; + } + + var retArr = []; + for(var n = 0; n < arr.length; n++) { + var node = arr[n]; + if(this.isAncestorOf(node)) { + retArr.push(node); + } + } + + return retArr; + }, + /** + * determine if node is an ancestor + * of descendant + * @name isAncestorOf + * @methodOf Kinetic.Container.prototype + * @param {Kinetic.Node} node + */ + isAncestorOf: function(node) { + if(this.nodeType === 'Stage') { + return true; + } + + var parent = node.getParent(); + while(parent) { + if(parent._id === this._id) { + return true; + } + parent = parent.getParent(); + } + + return false; + }, + /** + * get shapes that intersect a point + * @name getIntersections + * @methodOf Kinetic.Container.prototype + * @param {Object} point + */ + getIntersections: function() { + var pos = Kinetic.Type._getXY(Array.prototype.slice.call(arguments)); + var arr = []; + var shapes = this.get('Shape'); + + for(var n = 0; n < shapes.length; n++) { + var shape = shapes[n]; + if(shape.isVisible() && shape.intersects(pos)) { + arr.push(shape); + } + } + + return arr; + }, + /** + * get all shapes inside container + */ + _getNodes: function(sel) { + var arr = []; + function traverse(cont) { + var children = cont.getChildren(); + for(var n = 0; n < children.length; n++) { + var child = children[n]; + if(child.nodeType === sel) { + arr.push(child); + } + else if(child.nodeType !== 'Shape') { + traverse(child); + } + } + } + traverse(this); + + return arr; + }, + /** + * draw children + */ + _drawChildren: function(canvas) { + var stage = this.getStage(); + var children = this.children; + for(var n = 0; n < children.length; n++) { + var child = children[n]; + if(child.nodeType === 'Shape') { + if(child.isVisible() && stage.isVisible()) { + child._draw(canvas); + } + } + else { + child.draw(canvas); + } + } + }, + /** + * set children indices + */ + _setChildrenIndices: function() { + for(var n = 0; n < this.children.length; n++) { + this.children[n].index = n; + } + } +}); + +/////////////////////////////////////////////////////////////////////// +// Stage +/////////////////////////////////////////////////////////////////////// +/** + * Stage constructor. A stage is used to contain multiple layers and handle + * animations + * @constructor + * @augments Kinetic.Container + * @param {Object} config + * @param {String|DomElement} config.container Container id or DOM element + * @param {Number} config.width + * @param {Number} config.height + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.alpha] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] + * @param {Number} [config.scale.x] + * @param {Number} [config.scale.y] + * @param {Number} [config.rotation] rotation in radians + * @param {Number} [config.rotationDeg] rotation in degrees + * @param {Object} [config.offset] offsets default position point and rotation point + * @param {Number} [config.offset.x] + * @param {Number} [config.offset.y] + * @param {Boolean} [config.draggable] + * @param {String} [config.dragConstraint] can be vertical, horizontal, or none. The default + * is none + * @param {Object} [config.dragBounds] + * @param {Number} [config.dragBounds.top] + * @param {Number} [config.dragBounds.right] + * @param {Number} [config.dragBounds.bottom] + * @param {Number} [config.dragBounds.left] + */ +Kinetic.Stage = Kinetic.Container.extend({ + init: function(config) { + this.setDefaultAttrs({ + width: 400, + height: 200 + }); + + /* + * if container is a string, assume it's an id for + * a DOM element + */ + if( typeof config.container === 'string') { + config.container = document.getElementById(config.container); + } + + // call super constructor + this._super(config); + + this._setStageDefaultProperties(); + this._id = Kinetic.Global.idCounter++; + this._buildDOM(); + this._bindContentEvents(); + + //change events + this.on('widthChange.kinetic', function() { + this._resizeDOM(); + }); + + this.on('heightChange.kinetic', function() { + this._resizeDOM(); + }); + var go = Kinetic.Global; + go.stages.push(this); + this._addId(this); + this._addName(this); + + }, + /** + * sets onFrameFunc for animation + * @name onFrame + * @methodOf Kinetic.Stage.prototype + * @param {function} func + */ + onFrame: function(func) { + this.anim.func = func; + }, + /** + * start animation + * @name start + * @methodOf Kinetic.Stage.prototype + */ + start: function() { + if(!this.animRunning) { + var a = Kinetic.Animation; + a._addAnimation(this.anim); + a._handleAnimation(); + this.animRunning = true; + } + }, + /** + * stop animation + * @name stop + * @methodOf Kinetic.Stage.prototype + */ + stop: function() { + Kinetic.Animation._removeAnimation(this.anim); + this.animRunning = false; + }, + /** + * draw children + * @name draw + * @methodOf Kinetic.Stage.prototype + */ + draw: function(canvas) { + this._draw(canvas); + }, + /** + * set stage size + * @name setSize + * @methodOf Kinetic.Stage.prototype + * @param {Number} width + * @param {Number} height + */ + setSize: function() { + // set stage dimensions + var size = Kinetic.Type._getSize(Array.prototype.slice.call(arguments)); + this.setAttrs(size); + }, + /** + * get stage size + * @name getSize + * @methodOf Kinetic.Stage.prototype + */ + getSize: function() { + return { + width: this.attrs.width, + height: this.attrs.height + }; + }, + /** + * clear all layers + * @name clear + * @methodOf Kinetic.Stage.prototype + */ + clear: function() { + var layers = this.children; + for(var n = 0; n < layers.length; n++) { + layers[n].clear(); + } + }, + /** + * serialize stage and children as a JSON object and return + * the result as a json string + * @name toJSON + * @methodOf Kinetic.Stage.prototype + */ + toJSON: function() { + var type = Kinetic.Type; + + function addNode(node) { + var obj = {}; + + obj.attrs = {}; + + // serialize only attributes that are not function, image, DOM, or objects with methods + for(var key in node.attrs) { + var val = node.attrs[key]; + if(!type._isFunction(val) && !type._isElement(val) && !type._hasMethods(val)) { + obj.attrs[key] = val; + } + } + + obj.nodeType = node.nodeType; + obj.shapeType = node.shapeType; + + if(node.nodeType !== 'Shape') { + obj.children = []; + + var children = node.getChildren(); + for(var n = 0; n < children.length; n++) { + var child = children[n]; + obj.children.push(addNode(child)); + } + } + + return obj; + } + return JSON.stringify(addNode(this)); + }, + /** + * reset stage to default state + * @name reset + * @methodOf Kinetic.Stage.prototype + */ + reset: function() { + // remove children + this.removeChildren(); + + // defaults + this._setStageDefaultProperties(); + this.setAttrs(this.defaultNodeAttrs); + }, + /** + * load stage with JSON string. De-serializtion does not generate custom + * shape drawing functions, images, or event handlers (this would make the + * serialized object huge). If your app uses custom shapes, images, and + * event handlers (it probably does), then you need to select the appropriate + * shapes after loading the stage and set these properties via on(), setDrawFunc(), + * and setImage() + * @name load + * @methodOf Kinetic.Stage.prototype + * @param {String} JSON string + */ + load: function(json) { + this.reset(); + + function loadNode(node, obj) { + var children = obj.children; + if(children !== undefined) { + for(var n = 0; n < children.length; n++) { + var child = children[n]; + var type; + + // determine type + if(child.nodeType === 'Shape') { + // add custom shape + if(child.shapeType === undefined) { + type = 'Shape'; + } + // add standard shape + else { + type = child.shapeType; + } + } + else { + type = child.nodeType; + } + + var no = new Kinetic[type](child.attrs); + node.add(no); + loadNode(no, child); + } + } + } + var obj = JSON.parse(json); + + // copy over stage properties + this.attrs = obj.attrs; + + loadNode(this, obj); + this.draw(); + }, + /** + * get mouse position for desktop apps + * @name getMousePosition + * @methodOf Kinetic.Stage.prototype + * @param {Event} evt + */ + getMousePosition: function(evt) { + return this.mousePos; + }, + /** + * get touch position for mobile apps + * @name getTouchPosition + * @methodOf Kinetic.Stage.prototype + * @param {Event} evt + */ + getTouchPosition: function(evt) { + return this.touchPos; + }, + /** + * get user position (mouse position or touch position) + * @name getUserPosition + * @methodOf Kinetic.Stage.prototype + * @param {Event} evt + */ + getUserPosition: function(evt) { + return this.getTouchPosition() || this.getMousePosition(); + }, + /** + * get container DOM element + * @name getContainer + * @methodOf Kinetic.Stage.prototype + */ + getContainer: function() { + return this.attrs.container; + }, + /** + * get stage + * @name getStage + * @methodOf Kinetic.Stage.prototype + */ + getStage: function() { + return this; + }, + /** + * get stage DOM node, which is a div element + * with the class name "kineticjs-content" + * @name getDOM + * @methodOf Kinetic.Stage.prototype + */ + getDOM: function() { + return this.content; + }, + /** + * Creates a composite data URL and requires a callback because the stage + * toDataURL method is asynchronous. If MIME type is not + * specified, then "image/png" will result. For "image/jpeg", specify a quality + * level as quality (range 0.0 - 1.0). Note that this method works + * differently from toDataURL() for other nodes because it generates an absolute dataURL + * based on what's draw onto the canvases for each layer, rather than drawing + * the current state of each node + * @name toDataURL + * @methodOf Kinetic.Stage.prototype + * @param {Object} config + * @param {Function} config.callback since the stage toDataURL() method is asynchronous, + * the data url string will be passed into the callback + * @param {String} [config.mimeType] mime type. can be "image/png" or "image/jpeg". + * "image/png" is the default + * @param {Number} [config.width] data url image width + * @param {Number} [config.height] data url image height + * @param {Number} [config.quality] jpeg quality. If using an "image/jpeg" mimeType, + * you can specify the quality from 0 to 1, where 0 is very poor quality and 1 + * is very high quality + */ + toDataURL: function(config) { + var mimeType = config && config.mimeType ? config.mimeType : null; + var quality = config && config.quality ? config.quality : null; + /* + * need to create a canvas element rather than using the buffer canvas + * because this method is asynchonous which means that other parts of the + * code could modify the buffer canvas before it's finished + */ + var width = config && config.width ? config.width : this.attrs.width; + var height = config && config.height ? config.height : this.attrs.height; + var canvas = new Kinetic.Canvas(width, height); + var context = canvas.getContext(); + var layers = this.children; + + function drawLayer(n) { + var layer = layers[n]; + var layerUrl = layer.getCanvas().toDataURL(mimeType, quality); + var imageObj = new Image(); + imageObj.onload = function() { + context.drawImage(imageObj, 0, 0); + + if(n < layers.length - 1) { + drawLayer(n + 1); + } + else { + config.callback(canvas.toDataURL(mimeType, quality)); + } + }; + imageObj.src = layerUrl; + } + drawLayer(0); + }, + /** + * converts stage into an image. Since the stage toImage() method + * is asynchronous, a callback function is required + * @name toImage + * @methodOf Kinetic.Stage.prototype + * @param {Object} config + * @param {Function} callback since the toImage() method is asynchonrous, the + * resulting image object is passed into the callback function + * @param {String} [config.mimeType] mime type. can be "image/png" or "image/jpeg". + * "image/png" is the default + * @param {Number} [config.width] data url image width + * @param {Number} [config.height] data url image height + * @param {Number} [config.quality] jpeg quality. If using an "image/jpeg" mimeType, + * you can specify the quality from 0 to 1, where 0 is very poor quality and 1 + * is very high quality + */ + toImage: function(config) { + this.toDataURL({ + callback: function(dataUrl) { + Kinetic.Type._getImage(dataUrl, function(img) { + config.callback(img); + }); + } + }); + }, + _resizeDOM: function() { + var width = this.attrs.width; + var height = this.attrs.height; + + // set content dimensions + this.content.style.width = width + 'px'; + this.content.style.height = height + 'px'; + + // set buffer canvas and path canvas sizes + this.bufferCanvas.setSize(width, height); + this.pathCanvas.setSize(width, height); + + // set user defined layer dimensions + var layers = this.children; + for(var n = 0; n < layers.length; n++) { + var layer = layers[n]; + layer.getCanvas().setSize(width, height); + layer.draw(); + } + }, + /** + * remove layer from stage + * @param {Layer} layer + */ + _remove: function(layer) { + /* + * remove canvas DOM from the document if + * it exists + */ + try { + this.content.removeChild(layer.canvas.element); + } + catch(e) { + } + }, + /** + * add layer to stage + * @param {Layer} layer + */ + _add: function(layer) { + layer.canvas.setSize(this.attrs.width, this.attrs.height); + + // draw layer and append canvas to container + layer.draw(); + this.content.appendChild(layer.canvas.element); + + /* + * set layer last draw time to zero + * so that throttling doesn't take into account + * the layer draws associated with adding a node + */ + layer.lastDrawTime = 0; + }, + /** + * detect event + * @param {Shape} shape + */ + _detectEvent: function(shape, evt) { + var isDragging = Kinetic.Global.drag.moving; + var go = Kinetic.Global; + var pos = this.getUserPosition(); + var el = shape.eventListeners; + var that = this; + + if(this.targetShape && shape._id === this.targetShape._id) { + this.targetFound = true; + } + + if(shape.isVisible() && pos !== undefined && shape.intersects(pos)) { + // handle onmousedown + if(!isDragging && this.mouseDown) { + this.mouseDown = false; + this.clickStart = true; + shape._handleEvent('mousedown', evt); + return true; + } + // handle onmouseup & onclick + else if(this.mouseUp) { + this.mouseUp = false; + shape._handleEvent('mouseup', evt); + + // detect if click or double click occurred + if(this.clickStart) { + /* + * if dragging and dropping, don't fire click or dbl click + * event + */ + if((!go.drag.moving) || !go.drag.node) { + shape._handleEvent('click', evt); + + if(this.inDoubleClickWindow) { + shape._handleEvent('dblclick', evt); + } + this.inDoubleClickWindow = true; + setTimeout(function() { + that.inDoubleClickWindow = false; + }, this.dblClickWindow); + } + } + return true; + } + + // handle touchstart + else if(!isDragging && this.touchStart && !this.touchMove) { + this.touchStart = false; + this.tapStart = true; + shape._handleEvent('touchstart', evt); + return true; + } + // handle touchend & tap + else if(this.touchEnd) { + this.touchEnd = false; + shape._handleEvent('touchend', evt); + + // detect if tap or double tap occurred + if(this.tapStart) { + /* + * if dragging and dropping, don't fire tap or dbltap + * event + */ + if((!go.drag.moving) || !go.drag.node) { + shape._handleEvent('tap', evt); + + if(this.inDoubleClickWindow) { + shape._handleEvent('dbltap', evt); + } + this.inDoubleClickWindow = true; + setTimeout(function() { + that.inDoubleClickWindow = false; + }, this.dblClickWindow); + } + } + return true; + } + else if(!isDragging && this.touchMove) { + shape._handleEvent('touchmove', evt); + return true; + } + /* + * NOTE: these event handlers require target shape + * handling + */ + // handle onmouseover + else if(!isDragging && this._isNewTarget(shape, evt)) { + /* + * check to see if there are stored mouseout events first. + * if there are, run those before running the onmouseover + * events + */ + if(this.mouseoutShape) { + this.mouseoverShape = shape; + this.mouseoutShape._handleEvent('mouseout', evt); + this.mouseoverShape = undefined; + } + + shape._handleEvent('mouseover', evt); + this._setTarget(shape); + return true; + } + // handle mousemove and touchmove + else { + if(!isDragging && this.mouseMove) { + shape._handleEvent('mousemove', evt); + return true; + } + } + + } + // handle mouseout condition + else if(!isDragging && this.targetShape && this.targetShape._id === shape._id) { + this._setTarget(undefined); + this.mouseoutShape = shape; + return true; + } + + return false; + }, + /** + * set new target + */ + _setTarget: function(shape) { + this.targetShape = shape; + this.targetFound = true; + }, + /** + * check if shape should be a new target + */ + _isNewTarget: function(shape, evt) { + if(!this.targetShape || (!this.targetFound && shape._id !== this.targetShape._id)) { + /* + * check if old target has an onmouseout event listener + */ + if(this.targetShape) { + var oldEl = this.targetShape.eventListeners; + if(oldEl) { + this.mouseoutShape = this.targetShape; + } + } + return true; + } + else { + return false; + } + }, + /** + * traverse container children + * @param {Container} obj + */ + _traverseChildren: function(obj, evt) { + var children = obj.children; + // propapgate backwards through children + for(var i = children.length - 1; i >= 0; i--) { + var child = children[i]; + if(child.getListening()) { + if(child.nodeType === 'Shape') { + var exit = this._detectEvent(child, evt); + if(exit) { + return true; + } + } + else { + var exit = this._traverseChildren(child, evt); + if(exit) { + return true; + } + } + } + } + + return false; + }, + /** + * handle incoming event + * @param {Event} evt + */ + _handleStageEvent: function(evt) { + var go = Kinetic.Global; + if(!evt) { + evt = window.event; + } + + this._setMousePosition(evt); + this._setTouchPosition(evt); + this.pathCanvas.clear(); + + /* + * loop through layers. If at any point an event + * is triggered, break out + */ + this.targetFound = false; + var shapeDetected = false; + for(var n = this.children.length - 1; n >= 0; n--) { + var layer = this.children[n]; + if(layer.isVisible() && n >= 0 && layer.getListening()) { + if(this._traverseChildren(layer, evt)) { + shapeDetected = true; + break; + } + } + } + + /* + * if no shape was detected and a mouseout shape has been stored, + * then run the onmouseout event handlers + */ + if(!shapeDetected && this.mouseoutShape) { + this.mouseoutShape._handleEvent('mouseout', evt); + this.mouseoutShape = undefined; + } + }, + /** + * begin listening for events by adding event handlers + * to the container + */ + _bindContentEvents: function() { + var go = Kinetic.Global; + var that = this; + + var events = ['mousedown', 'mousemove', 'mouseup', 'mouseover', 'mouseout', 'touchstart', 'touchmove', 'touchend']; + + for(var n = 0; n < events.length; n++) { + var pubEvent = events[n]; + // induce scope + ( function() { + var event = pubEvent; + that.content.addEventListener(event, function(evt) { + that['_' + event](evt); + }, false); + }()); + } + }, + _mouseover: function(evt) { + this._handleStageEvent(evt); + }, + _mouseout: function(evt) { + // if there's a current target shape, run mouseout handlers + var targetShape = this.targetShape; + if(targetShape) { + targetShape._handleEvent('mouseout', evt); + this.targetShape = undefined; + } + this.mousePos = undefined; + + // end drag and drop + this._endDrag(evt); + }, + _mousemove: function(evt) { + this.mouseDown = false; + this.mouseUp = false; + this.mouseMove = true; + this._handleStageEvent(evt); + + // start drag and drop + this._startDrag(evt); + }, + _mousedown: function(evt) { + this.mouseDown = true; + this.mouseUp = false; + this.mouseMove = false; + this._handleStageEvent(evt); + + //init stage drag and drop + if(this.attrs.draggable) { + this._initDrag(); + } + }, + _mouseup: function(evt) { + this.mouseDown = false; + this.mouseUp = true; + this.mouseMove = false; + this._handleStageEvent(evt); + this.clickStart = false; + + // end drag and drop + this._endDrag(evt); + }, + _touchstart: function(evt) { + evt.preventDefault(); + this.touchStart = true; + this.touchEnd = false; + this.touchMove = false; + this._handleStageEvent(evt); + /* + * init stage drag and drop + */ + if(this.attrs.draggable) { + this._initDrag(); + } + }, + _touchend: function(evt) { + this.touchStart = false; + this.touchEnd = true; + this.touchMove = false; + this._handleStageEvent(evt); + this.tapStart = false; + + // end drag and drop + this._endDrag(evt); + }, + _touchmove: function(evt) { + evt.preventDefault(); + this.touchEnd = false; + this.touchMove = true; + this._handleStageEvent(evt); + + // start drag and drop + this._startDrag(evt); + }, + /** + * set mouse positon for desktop apps + * @param {Event} evt + */ + _setMousePosition: function(evt) { + var mouseX = evt.offsetX || (evt.clientX - this._getContentPosition().left + window.pageXOffset); + var mouseY = evt.offsetY || (evt.clientY - this._getContentPosition().top + window.pageYOffset); + this.mousePos = { + x: mouseX, + y: mouseY + }; + }, + /** + * set touch position for mobile apps + * @param {Event} evt + */ + _setTouchPosition: function(evt) { + if(evt.touches !== undefined && evt.touches.length === 1) {// Only deal with + // one finger + var touch = evt.touches[0]; + // Get the information for finger #1 + var touchX = touch.clientX - this._getContentPosition().left + window.pageXOffset; + var touchY = touch.clientY - this._getContentPosition().top + window.pageYOffset; + + this.touchPos = { + x: touchX, + y: touchY + }; + } + }, + /** + * get container position + */ + _getContentPosition: function() { + var rect = this.content.getBoundingClientRect(), root = document.documentElement; + return { + top: rect.top + root.scrollTop, + left: rect.left + root.scrollLeft + }; + }, + /** + * end drag and drop + */ + _endDrag: function(evt) { + var go = Kinetic.Global; + if(go.drag.node) { + // handle dragend + if(go.drag.moving) { + go.drag.moving = false; + go.drag.node._handleEvent('dragend', evt); + } + } + go.drag.node = undefined; + }, + /** + * start drag and drop + */ + _startDrag: function(evt) { + var that = this; + var go = Kinetic.Global; + var node = go.drag.node; + + if(node) { + var dragThrottle = node.attrs.dragThrottle; + var time = new Date().getTime(); + var timeDiff = time - node.lastDragTime; + var tt = 1000 / dragThrottle; + if((timeDiff >= tt || dragThrottle > 200)) { + var pos = that.getUserPosition(); + var dc = node.attrs.dragConstraint; + var db = node.attrs.dragBounds; + var lastNodePos = { + x: node.attrs.x, + y: node.attrs.y + }; + + // default + var newNodePos = { + x: pos.x - go.drag.offset.x, + y: pos.y - go.drag.offset.y + }; + + // bounds overrides + if(db.left !== undefined && newNodePos.x < db.left) { + newNodePos.x = db.left; + } + if(db.right !== undefined && newNodePos.x > db.right) { + newNodePos.x = db.right; + } + if(db.top !== undefined && newNodePos.y < db.top) { + newNodePos.y = db.top; + } + if(db.bottom !== undefined && newNodePos.y > db.bottom) { + newNodePos.y = db.bottom; + } + + node.setAbsolutePosition(newNodePos); + + // constraint overrides + if(dc === 'horizontal') { + node.attrs.y = lastNodePos.y; + } + else if(dc === 'vertical') { + node.attrs.x = lastNodePos.x; + } + + /* + * if dragging and dropping the stage, + * draw all of the layers + */ + if(go.drag.node.nodeType === 'Stage') { + go.drag.node.draw(); + } + + else { + go.drag.node.getLayer().draw(); + } + + if(!go.drag.moving) { + go.drag.moving = true; + // execute dragstart events if defined + go.drag.node._handleEvent('dragstart', evt); + } + + // execute user defined ondragmove if defined + go.drag.node._handleEvent('dragmove', evt); + + node.lastDragTime = new Date().getTime(); + } + } + }, + /** + * build dom + */ + _buildDOM: function() { + // content + this.content = document.createElement('div'); + this.content.style.position = 'relative'; + this.content.style.display = 'inline-block'; + this.content.className = 'kineticjs-content'; + this.attrs.container.appendChild(this.content); + + this.bufferCanvas = new Kinetic.Canvas({ + width: this.attrs.width, + height: this.attrs.height + }); + this.pathCanvas = new Kinetic.Canvas({ + width: this.attrs.width, + height: this.attrs.height + }); + this.pathCanvas.strip(); + this._resizeDOM(); + }, + _addId: function(node) { + if(node.attrs.id !== undefined) { + this.ids[node.attrs.id] = node; + } + }, + _removeId: function(id) { + if(id !== undefined) { + delete this.ids[id]; + } + }, + _addName: function(node) { + var name = node.attrs.name; + if(name !== undefined) { + if(this.names[name] === undefined) { + this.names[name] = []; + } + this.names[name].push(node); + } + }, + _removeName: function(name, _id) { + if(name !== undefined) { + var nodes = this.names[name]; + if(nodes !== undefined) { + for(var n = 0; n < nodes.length; n++) { + var no = nodes[n]; + if(no._id === _id) { + nodes.splice(n, 1); + } + } + if(nodes.length === 0) { + delete this.names[name]; + } + } + } + }, + /** + * bind event listener to container DOM element + * @param {String} typesStr + * @param {function} handler + */ + _onContent: function(typesStr, handler) { + var types = typesStr.split(' '); + for(var n = 0; n < types.length; n++) { + var baseEvent = types[n]; + this.content.addEventListener(baseEvent, handler, false); + } + }, + /** + * set defaults + */ + _setStageDefaultProperties: function() { + this.nodeType = 'Stage'; + this.dblClickWindow = 400; + this.targetShape = undefined; + this.targetFound = false; + this.mouseoverShape = undefined; + this.mouseoutShape = undefined; + + // desktop flags + this.mousePos = undefined; + this.mouseDown = false; + this.mouseUp = false; + this.mouseMove = false; + this.clickStart = false; + + // mobile flags + this.touchPos = undefined; + this.touchStart = false; + this.touchEnd = false; + this.touchMove = false; + this.tapStart = false; + + this.ids = {}; + this.names = {}; + this.anim = new Kinetic.Animation(); + this.animRunning = false; + }, + _draw: function(canvas) { + this._drawChildren(canvas); + } +}); + +// add getters and setters +Kinetic.Node.addGettersSetters(Kinetic.Stage, ['width', 'height']); + +/** + * get width + * @name getWidth + * @methodOf Kinetic.Stage.prototype + */ + +/** + * get height + * @name getHeight + * @methodOf Kinetic.Stage.prototype + */ + +/** + * set width + * @name setWidth + * @methodOf Kinetic.Stage.prototype + * @param {Number} width + */ + +/** + * set height + * @name setHeight + * @methodOf Kinetic.Stage.prototype + * @param {Number} height + */ +/////////////////////////////////////////////////////////////////////// +// Layer +/////////////////////////////////////////////////////////////////////// +/** + * Layer constructor. Layers are tied to their own canvas element and are used + * to contain groups or shapes + * @constructor + * @augments Kinetic.Container + * @param {Object} config + * @param {Boolean} [config.clearBeforeDraw] set this property to true if you'd like to disable + * canvas clearing before each new layer draw + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.alpha] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] + * @param {Number} [config.scale.x] + * @param {Number} [config.scale.y] + * @param {Number} [config.rotation] rotation in radians + * @param {Number} [config.rotationDeg] rotation in degrees + * @param {Object} [config.offset] offsets default position point and rotation point + * @param {Number} [config.offset.x] + * @param {Number} [config.offset.y] + * @param {Boolean} [config.draggable] + * @param {String} [config.dragConstraint] can be vertical, horizontal, or none. The default + * is none + * @param {Object} [config.dragBounds] + * @param {Number} [config.dragBounds.top] + * @param {Number} [config.dragBounds.right] + * @param {Number} [config.dragBounds.bottom] + * @param {Number} [config.dragBounds.left] + */ +Kinetic.Layer = Kinetic.Container.extend({ + init: function(config) { + this.setDefaultAttrs({ + clearBeforeDraw: true + }); + + this.nodeType = 'Layer'; + this.lastDrawTime = 0; + this.beforeDrawFunc = undefined; + this.afterDrawFunc = undefined; + + this.canvas = new Kinetic.Canvas(); + this.canvas.getElement().style.position = 'absolute'; + + // call super constructor + this._super(config); + }, + /** + * draw children nodes. this includes any groups + * or shapes + * @name draw + * @methodOf Kinetic.Layer.prototype + */ + draw: function(canvas) { + this._draw(canvas); + }, + /** + * set before draw handler + * @name beforeDraw + * @methodOf Kinetic.Layer.prototype + * @param {Function} handler + */ + beforeDraw: function(func) { + this.beforeDrawFunc = func; + }, + /** + * set after draw handler + * @name afterDraw + * @methodOf Kinetic.Layer.prototype + * @param {Function} handler + */ + afterDraw: function(func) { + this.afterDrawFunc = func; + }, + /** + * get layer canvas + * @name getCanvas + * @methodOf Kinetic.Layer.prototype + */ + getCanvas: function() { + return this.canvas; + }, + /** + * get layer canvas context + * @name getContext + * @methodOf Kinetic.Layer.prototype + */ + getContext: function() { + return this.canvas.context; + }, + /** + * clear canvas tied to the layer + * @name clear + * @methodOf Kinetic.Layer.prototype + */ + clear: function() { + this.getCanvas().clear(); + }, + /** + * Creates a composite data URL. If MIME type is not + * specified, then "image/png" will result. For "image/jpeg", specify a quality + * level as quality (range 0.0 - 1.0). Note that this method works + * differently from toDataURL() for other nodes because it generates an absolute dataURL + * based on what's draw on the layer, rather than drawing + * the current state of each child node + * @name toDataURL + * @methodOf Kinetic.Layer.prototype + * @param {Object} config + * @param {String} [config.mimeType] mime type. can be "image/png" or "image/jpeg". + * "image/png" is the default + * @param {Number} [config.width] data url image width + * @param {Number} [config.height] data url image height + * @param {Number} [config.quality] jpeg quality. If using an "image/jpeg" mimeType, + * you can specify the quality from 0 to 1, where 0 is very poor quality and 1 + * is very high quality + */ + toDataURL: function(config) { + var canvas; + var mimeType = config && config.mimeType ? config.mimeType : null; + var quality = config && config.quality ? config.quality : null; + + if(config && config.width && config.height) { + canvas = new Kinetic.Canvas(config.width, config.height); + } + else { + canvas = this.getCanvas(); + } + return canvas.toDataURL(mimeType, quality); + }, + /** + * private draw children + */ + _draw: function(canvas) { + /* + * if canvas is not defined, then use the canvas + * tied to the layer + */ + if(!canvas) { + canvas = this.getCanvas(); + } + + var time = new Date().getTime(); + this.lastDrawTime = time; + + // before draw handler + if(this.beforeDrawFunc !== undefined) { + this.beforeDrawFunc.call(this); + } + + if(this.attrs.clearBeforeDraw) { + canvas.clear(); + } + + if(this.isVisible()) { + // draw custom func + if(this.attrs.drawFunc !== undefined) { + this.attrs.drawFunc.call(this); + } + + // draw children + this._drawChildren(canvas); + } + + // after draw handler + if(this.afterDrawFunc !== undefined) { + this.afterDrawFunc.call(this); + } + } +}); + +// add getters and setters +Kinetic.Node.addGettersSetters(Kinetic.Layer, ['clearBeforeDraw']); + +/** + * set flag which determines if the layer is cleared or not + * before drawing + * @name setClearBeforeDraw + * @methodOf Kinetic.Layer.prototype + * @param {Boolean} clearBeforeDraw + */ + +/** + * get flag which determines if the layer is cleared or not + * before drawing + * @name getClearBeforeDraw + * @methodOf Kinetic.Layer.prototype + */ +/////////////////////////////////////////////////////////////////////// +// Group +/////////////////////////////////////////////////////////////////////// +/** + * Group constructor. Groups are used to contain shapes or other groups. + * @constructor + * @augments Kinetic.Container + * @param {Object} config + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.alpha] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] + * @param {Number} [config.scale.x] + * @param {Number} [config.scale.y] + * @param {Number} [config.rotation] rotation in radians + * @param {Number} [config.rotationDeg] rotation in degrees + * @param {Object} [config.offset] offsets default position point and rotation point + * @param {Number} [config.offset.x] + * @param {Number} [config.offset.y] + * @param {Boolean} [config.draggable] + * @param {String} [config.dragConstraint] can be vertical, horizontal, or none. The default + * is none + * @param {Object} [config.dragBounds] + * @param {Number} [config.dragBounds.top] + * @param {Number} [config.dragBounds.right] + * @param {Number} [config.dragBounds.bottom] + * @param {Number} [config.dragBounds.left] + */ +Kinetic.Group = Kinetic.Container.extend({ + init: function(config) { + this.nodeType = 'Group'; + + // call super constructor + this._super(config); + }, + draw: function(canvas) { + this._draw(canvas); + }, + _draw: function(canvas) { + if(this.attrs.visible) { + this._drawChildren(canvas); + } + } +}); + +/////////////////////////////////////////////////////////////////////// +// Shape +/////////////////////////////////////////////////////////////////////// +/** + * Shape constructor. Shapes are primitive objects such as rectangles, + * circles, text, lines, etc. + * @constructor + * @augments Kinetic.Node + * @param {Object} config + * @config {String|Object} [config.fill] can be a string color, a linear gradient object, a radial + * gradient object, or a pattern object. + * @config {Image} [config.fill.image] image object if filling the shape with a pattern + * @config {Object} [config.fill.offset] pattern offset if filling the shape with a pattern + * @config {Number} [config.fill.offset.x] + * @config {Number} [config.fill.offset.y] + * @config {Object} [config.fill.start] start point if using a linear gradient or + * radial gradient fill + * @config {Number} [config.fill.start.x] + * @config {Number} [config.fill.start.y] + * @config {Number} [config.fill.start.radius] start radius if using a radial gradient fill + * @config {Object} [config.fill.end] end point if using a linear gradient or + * radial gradient fill + * @config {Number} [config.fill.end.x] + * @config {Number} [config.fill.end.y] + * @config {Number} [config.fill.end.radius] end radius if using a radial gradient fill + * @config {String} [config.stroke] stroke color + * @config {Number} [config.strokeWidth] stroke width + * @config {String} [config.lineJoin] line join can be miter, round, or bevel. The default + * is miter + * @config {Object} [config.shadow] shadow object + * @config {String} [config.shadow.color] + * @config {Number} [config.shadow.blur] + * @config {Obect} [config.shadow.blur.offset] + * @config {Number} [config.shadow.blur.offset.x] + * @config {Number} [config.shadow.blur.offset.y] + * @config {Number} [config.shadow.alpha] shadow alpha. Can be any real number + * between 0 and 1 + * @config {String} [config.detectionType] shape detection type. Can be path or pixel. + * The default is path because it performs better + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.alpha] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] + * @param {Number} [config.scale.x] + * @param {Number} [config.scale.y] + * @param {Number} [config.rotation] rotation in radians + * @param {Number} [config.rotationDeg] rotation in degrees + * @param {Object} [config.offset] offsets default position point and rotation point + * @param {Number} [config.offset.x] + * @param {Number} [config.offset.y] + * @param {Boolean} [config.draggable] + * @param {String} [config.dragConstraint] can be vertical, horizontal, or none. The default + * is none + * @param {Object} [config.dragBounds] + * @param {Number} [config.dragBounds.top] + * @param {Number} [config.dragBounds.right] + * @param {Number} [config.dragBounds.bottom] + * @param {Number} [config.dragBounds.left] + */ +Kinetic.Shape = Kinetic.Node.extend({ + init: function(config) { + this.setDefaultAttrs({ + detectionType: 'path' + }); + + this.nodeType = 'Shape'; + this.appliedShadow = false; + + // call super constructor + this._super(config); + }, + /** + * get canvas context tied to the layer + * @name getContext + * @methodOf Kinetic.Shape.prototype + */ + getContext: function() { + return this.getLayer().getContext(); + }, + /** + * get canvas tied to the layer + * @name getCanvas + * @methodOf Kinetic.Shape.prototype + */ + getCanvas: function() { + return this.getLayer().getCanvas(); + }, + /** + * helper method to stroke the shape and apply + * shadows if needed + * @name stroke + * @methodOf Kinetic.Shape.prototype + */ + stroke: function(context) { + var strokeWidth = this.getStrokeWidth(); + var stroke = this.getStroke(); + if(stroke || strokeWidth) { + var go = Kinetic.Global; + var appliedShadow = false; + + context.save(); + if(this.attrs.shadow && !this.appliedShadow) { + appliedShadow = this._applyShadow(context); + } + + context.lineWidth = strokeWidth || 2; + context.strokeStyle = stroke || 'black'; + context.stroke(context); + context.restore(); + + if(appliedShadow) { + this.stroke(context); + } + } + }, + /** + * helper method to fill the shape with a color, linear gradient, + * radial gradient, or pattern, and also apply shadows if needed + * @name fill + * @methodOf Kinetic.Shape.prototype + * */ + fill: function(context) { + var appliedShadow = false; + var fill = this.attrs.fill; + if(fill) { + context.save(); + if(this.attrs.shadow && !this.appliedShadow) { + appliedShadow = this._applyShadow(context); + } + + var s = fill.start; + var e = fill.end; + var f = null; + + // color fill + if(Kinetic.Type._isString(fill)) { + context.fillStyle = fill; + context.fill(context); + } + // pattern + else if(fill.image) { + var repeat = !fill.repeat ? 'repeat' : fill.repeat; + if(fill.scale) { + context.scale(fill.scale.x, fill.scale.y); + } + if(fill.offset) { + context.translate(fill.offset.x, fill.offset.y); + } + + context.fillStyle = context.createPattern(fill.image, repeat); + context.fill(context); + } + // linear gradient + else if(!s.radius && !e.radius) { + var grd = context.createLinearGradient(s.x, s.y, e.x, e.y); + var colorStops = fill.colorStops; + + // build color stops + for(var n = 0; n < colorStops.length; n += 2) { + grd.addColorStop(colorStops[n], colorStops[n + 1]); + } + context.fillStyle = grd; + context.fill(context); + } + // radial gradient + else if((s.radius || s.radius === 0) && (e.radius || e.radius === 0)) { + var grd = context.createRadialGradient(s.x, s.y, s.radius, e.x, e.y, e.radius); + var colorStops = fill.colorStops; + + // build color stops + for(var n = 0; n < colorStops.length; n += 2) { + grd.addColorStop(colorStops[n], colorStops[n + 1]); + } + context.fillStyle = grd; + context.fill(context); + } + else { + context.fillStyle = 'black'; + context.fill(context); + } + context.restore(); + } + + if(appliedShadow) { + this.fill(context); + } + }, + /** + * helper method to fill text and appy shadows if needed + * @param {String} text + * @name fillText + * @methodOf Kinetic.Shape.prototype + */ + fillText: function(context, text) { + var appliedShadow = false; + if(this.attrs.textFill) { + context.save(); + if(this.attrs.shadow && !this.appliedShadow) { + appliedShadow = this._applyShadow(context); + } + context.fillStyle = this.attrs.textFill; + context.fillText(text, 0, 0); + context.restore(); + } + if(appliedShadow) { + this.fillText(context, text, 0, 0); + } + }, + /** + * helper method to stroke text and apply shadows + * if needed + * @name strokeText + * @methodOf Kinetic.Shape.prototype + * @param {String} text + */ + strokeText: function(context, text) { + var appliedShadow = false; + + if(this.attrs.textStroke || this.attrs.textStrokeWidth) { + context.save(); + if(this.attrs.shadow && !this.appliedShadow) { + appliedShadow = this._applyShadow(context); + } + // defaults + var textStroke = this.attrs.textStroke ? this.attrs.textStroke : 'black'; + var textStrokeWidth = this.attrs.textStrokeWidth ? this.attrs.textStrokeWidth : 2; + context.lineWidth = textStrokeWidth; + context.strokeStyle = textStroke; + context.strokeText(text, 0, 0); + context.restore(); + } + + if(appliedShadow) { + this.strokeText(context, text, 0, 0); + } + }, + /** + * helper method to draw an image and apply + * a shadow if neede + * @name drawImage + * @methodOf Kinetic.Shape.prototype + */ + drawImage: function() { + var appliedShadow = false; + var context = arguments[0]; + context.save(); + var a = Array.prototype.slice.call(arguments); + + if(a.length === 6 || a.length === 10) { + if(this.attrs.shadow && !this.appliedShadow) { + appliedShadow = this._applyShadow(context); + } + + if(a.length === 6) { + context.drawImage(a[1], a[2], a[3], a[4], a[5]); + } + else { + context.drawImage(a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], a[9]); + } + } + + context.restore(); + + if(appliedShadow) { + this.drawImage.apply(this, a); + } + }, + /** + * helper method to set the line join of a shape + * based on the lineJoin property + * @name applyLineJoin + * @methodOf Kinetic.Shape.prototype + */ + applyLineJoin: function(context) { + if(this.attrs.lineJoin) { + context.lineJoin = this.attrs.lineJoin; + } + }, + /** + * apply shadow. return true if shadow was applied + * and false if it was not + */ + _applyShadow: function(context) { + var s = this.attrs.shadow; + if(s) { + var aa = this.getAbsoluteAlpha(); + // defaults + var color = s.color ? s.color : 'black'; + var blur = s.blur ? s.blur : 5; + var offset = s.offset ? s.offset : { + x: 0, + y: 0 + }; + + if(s.alpha) { + context.globalAlpha = s.alpha * aa; + } + context.shadowColor = color; + context.shadowBlur = blur; + context.shadowOffsetX = offset.x; + context.shadowOffsetY = offset.y; + this.appliedShadow = true; + return true; + } + + return false; + }, + /** + * determines if point is in the shape + * @param {Object|Array} point point can be an object containing + * an x and y property, or it can be an array with two elements + * in which the first element is the x component and the second + * element is the y component + */ + intersects: function() { + var pos = Kinetic.Type._getXY(Array.prototype.slice.call(arguments)); + var stage = this.getStage(); + + // path detection + if(this.attrs.detectionType === 'path') { + var pathCanvas = stage.pathCanvas; + var pathCanvasContext = pathCanvas.getContext(); + + this._draw(pathCanvas); + + return pathCanvasContext.isPointInPath(pos.x, pos.y); + } + + // pixel detection + if(this.imageData) { + var w = stage.attrs.width; + var alpha = this.imageData.data[((w * pos.y) + pos.x) * 4 + 3]; + return (alpha); + } + + // default + return false; + }, + _draw: function(canvas) { + if(this.attrs.drawFunc) { + var stage = this.getStage(); + var context = canvas.getContext(); + var family = []; + var parent = this.parent; + + family.unshift(this); + while(parent) { + family.unshift(parent); + parent = parent.parent; + } + + context.save(); + for(var n = 0; n < family.length; n++) { + var node = family[n]; + var t = node.getTransform(); + var m = t.getMatrix(); + context.transform(m[0], m[1], m[2], m[3], m[4], m[5]); + } + + /* + * pre styles include alpha, linejoin + */ + var absAlpha = this.getAbsoluteAlpha(); + if(absAlpha !== 1) { + context.globalAlpha = absAlpha; + } + this.applyLineJoin(context); + + // draw the shape + this.appliedShadow = false; + this.attrs.drawFunc.call(this, canvas.getContext()); + context.restore(); + } + } +}); + +// add getters and setters +Kinetic.Node.addGettersSetters(Kinetic.Shape, ['fill', 'stroke', 'lineJoin', 'strokeWidth', 'shadow', 'drawFunc', 'filter']); + +/** + * set fill which can be a color, linear gradient object, + * radial gradient object, or pattern object + * @name setFill + * @methodOf Kinetic.Shape.prototype + * @param {String|Object} fill + */ + +/** + * set stroke color + * @name setStroke + * @methodOf Kinetic.Shape.prototype + * @param {String} stroke + */ + +/** + * set line join + * @name setLineJoin + * @methodOf Kinetic.Shape.prototype + * @param {String} lineJoin. Can be miter, round, or bevel. The + * default is miter + */ + +/** + * set stroke width + * @name setStrokeWidth + * @methodOf Kinetic.Shape.prototype + * @param {Number} strokeWidth + */ + +/** + * set shadow object + * @name setShadow + * @methodOf Kinetic.Shape.prototype + * @param {Object} config + */ + +/** + * set draw function + * @name setDrawFunc + * @methodOf Kinetic.Shape.prototype + * @param {Function} drawFunc drawing function + */ + +/** + * get fill + * @name getFill + * @methodOf Kinetic.Shape.prototype + */ + +/** + * get stroke color + * @name getStroke + * @methodOf Kinetic.Shape.prototype + */ + +/** + * get line join + * @name getLineJoin + * @methodOf Kinetic.Shape.prototype + */ + +/** + * get stroke width + * @name getStrokeWidth + * @methodOf Kinetic.Shape.prototype + */ + +/** + * get shadow object + * @name getShadow + * @methodOf Kinetic.Shape.prototype + */ + +/** + * get draw function + * @name getDrawFunc + * @methodOf Kinetic.Shape.prototype + */ +/////////////////////////////////////////////////////////////////////// +// Rect +/////////////////////////////////////////////////////////////////////// +/** + * Rect constructor + * @constructor + * @augments Kinetic.Shape + * @param {Object} config + */ +Kinetic.Rect = Kinetic.Shape.extend({ + init: function(config) { + this.setDefaultAttrs({ + width: 0, + height: 0, + cornerRadius: 0 + }); + this.shapeType = "Rect"; + config.drawFunc = this.drawFunc; + // call super constructor + this._super(config); + }, + drawFunc: function(context) { + context.beginPath(); + if(this.attrs.cornerRadius === 0) { + // simple rect - don't bother doing all that complicated maths stuff. + context.rect(0, 0, this.attrs.width, this.attrs.height); + } + else { + // arcTo would be nicer, but browser support is patchy (Opera) + context.moveTo(this.attrs.cornerRadius, 0); + context.lineTo(this.attrs.width - this.attrs.cornerRadius, 0); + context.arc(this.attrs.width - this.attrs.cornerRadius, this.attrs.cornerRadius, this.attrs.cornerRadius, Math.PI * 3 / 2, 0, false); + context.lineTo(this.attrs.width, this.attrs.height - this.attrs.cornerRadius); + context.arc(this.attrs.width - this.attrs.cornerRadius, this.attrs.height - this.attrs.cornerRadius, this.attrs.cornerRadius, 0, Math.PI / 2, false); + context.lineTo(this.attrs.cornerRadius, this.attrs.height); + context.arc(this.attrs.cornerRadius, this.attrs.height - this.attrs.cornerRadius, this.attrs.cornerRadius, Math.PI / 2, Math.PI, false); + context.lineTo(0, this.attrs.cornerRadius); + context.arc(this.attrs.cornerRadius, this.attrs.cornerRadius, this.attrs.cornerRadius, Math.PI, Math.PI * 3 / 2, false); + } + context.closePath(); + + this.fill(context); + this.stroke(context); + }, + /** + * set width and height + * @name setSize + * @methodOf Kinetic.Rect.prototype + */ + setSize: function() { + var size = Kinetic.Type._getSize(Array.prototype.slice.call(arguments)); + this.setAttrs(size); + }, + /** + * return rect size + * @name getSize + * @methodOf Kinetic.Rect.prototype + */ + getSize: function() { + return { + width: this.attrs.width, + height: this.attrs.height + }; + } +}); + +// add getters setters +Kinetic.Node.addGettersSetters(Kinetic.Rect, ['width', 'height', 'cornerRadius']); + +/** + * set width + * @name setWidth + * @methodOf Kinetic.Rect.prototype + * @param {Number} width + */ + +/** + * set height + * @name setHeight + * @methodOf Kinetic.Rect.prototype + * @param {Number} height + */ + +/** + * set corner radius + * @name setCornerRadius + * @methodOf Kinetic.Rect.prototype + * @param {Number} radius + */ + +/** + * get width + * @name getWidth + * @methodOf Kinetic.Rect.prototype + */ + +/** + * get height + * @name getHeight + * @methodOf Kinetic.Rect.prototype + */ + +/** + * get corner radius + * @name getCornerRadius + * @methodOf Kinetic.Rect.prototype + */ +/////////////////////////////////////////////////////////////////////// +// Ellipse +/////////////////////////////////////////////////////////////////////// +/** + * Ellipse constructor + * @constructor + * @augments Kinetic.Shape + * @param {Object} config + */ +Kinetic.Ellipse = Kinetic.Shape.extend({ + init: function(config) { + this.setDefaultAttrs({ + radius: { + x: 0, + y: 0 + } + }); + + this.shapeType = "Ellipse"; + config.drawFunc = this.drawFunc; + + // call super constructor + this._super(config); + this._convertRadius(); + var that = this; + this.on('radiusChange.kinetic', function() { + that._convertRadius(); + }); + }, + drawFunc: function(context) { + var r = this.getRadius(); + context.beginPath(); + context.save(); + if(r.x !== r.y) { + context.scale(1, r.y / r.x); + } + context.arc(0, 0, r.x, 0, Math.PI * 2, true); + context.restore(); + context.closePath(); + this.fill(context); + this.stroke(context); + }, + /** + * converts numeric radius into an object + */ + _convertRadius: function() { + var type = Kinetic.Type; + var radius = this.getRadius(); + // if radius is already an object then return + if(type._isObject(radius)) { + return false; + } + + /* + * directly set radius attr to avoid + * duplicate attr change event + */ + this.attrs.radius = type._getXY(radius); + } +}); + +// Circle backwards compatibility +Kinetic.Circle = Kinetic.Ellipse; + +// add getters setters +Kinetic.Node.addGettersSetters(Kinetic.Ellipse, ['radius']); + +/** + * set radius + * @name setRadius + * @methodOf Kinetic.Ellipse.prototype + * @param {Number|Object|Array} radius + * radius can be a number, in which the ellipse becomes a circle, + * it can be an object with an x and y component, or it + * can be an array in which the first element is the x component + * and the second element is the y component. The x component + * defines the horizontal radius and the y component + * defines the vertical radius + */ + +/** + * get radius + * @name getRadius + * @methodOf Kinetic.Ellipse.prototype + */ +/////////////////////////////////////////////////////////////////////// +// Image +/////////////////////////////////////////////////////////////////////// +/** + * Image constructor + * @constructor + * @augments Kinetic.Shape + * @param {Object} config + * @param {ImageObject} config.image + * @param {Number} [config.width] + * @param {Number} [config.height] + * @param {Object} [config.crop] + */ +Kinetic.Image = Kinetic.Shape.extend({ + init: function(config) { + this.shapeType = "Image"; + config.drawFunc = this.drawFunc; + // call super constructor + this._super(config); + }, + drawFunc: function(context) { + if(this.attrs.image) { + var width = this.getWidth(); + var height = this.getHeight(); + + context.beginPath(); + context.rect(0, 0, width, height); + context.closePath(); + this.fill(context); + this.stroke(context); + + // if cropping + if(this.attrs.crop && this.attrs.crop.width && this.attrs.crop.height) { + var cropX = this.attrs.crop.x ? this.attrs.crop.x : 0; + var cropY = this.attrs.crop.y ? this.attrs.crop.y : 0; + var cropWidth = this.attrs.crop.width; + var cropHeight = this.attrs.crop.height; + this.drawImage(context, this.attrs.image, cropX, cropY, cropWidth, cropHeight, 0, 0, width, height); + } + // no cropping + else { + this.drawImage(context, this.attrs.image, 0, 0, width, height); + } + } + }, + /** + * set width and height + * @name setSize + * @methodOf Kinetic.Image.prototype + */ + setSize: function() { + var size = Kinetic.Type._getSize(Array.prototype.slice.call(arguments)); + this.setAttrs(size); + }, + /** + * return image size + * @name getSize + * @methodOf Kinetic.Image.prototype + */ + getSize: function() { + return { + width: this.attrs.width, + height: this.attrs.height + }; + }, + /** + * get width + * @name getWidth + * @methodOf Kinetic.Image.prototype + */ + getWidth: function() { + if(this.attrs.width) { + return this.attrs.width; + } + if(this.attrs.image) { + return this.attrs.image.width; + } + return 0; + }, + /** + * get height + * @name getHeight + * @methodOf Kinetic.Image.prototype + */ + getHeight: function() { + if(this.attrs.height) { + return this.attrs.height; + } + if(this.attrs.image) { + return this.attrs.image.height; + } + return 0; + }, + /** + * apply filter + * @name applyFilter + * @methodOf Kinetic.Image.prototype + * @param {Object} config + * @param {Function} config.filter filter function + * @param {Function} [config.callback] callback function to be called once + * filter has been applied + */ + applyFilter: function(config) { + try { + var trans = this._clearTransform(); + this.saveImageData(this.getWidth(), this.getHeight()); + this._setTransform(trans); + + config.filter.call(this, config); + var that = this; + Kinetic.Type._getImage(this.getImageData(), function(imageObj) { + that.setImage(imageObj); + + if(config.callback) { + config.callback(); + } + }); + } + catch(e) { + Kinetic.Global.warn('Unable to apply filter.'); + } + } +}); + +// add getters setters +Kinetic.Node.addGettersSetters(Kinetic.Image, ['image', 'crop', 'filter']); +Kinetic.Node.addSetters(Kinetic.Image, ['width', 'height']); + +/** + * set width + * @name setWidth + * @methodOf Kinetic.Image.prototype + * @param {Number} width + */ + +/** + * set height + * @name setHeight + * @methodOf Kinetic.Image.prototype + * @param {Number} height + */ + +/** + * set image + * @name setImage + * @methodOf Kinetic.Image.prototype + * @param {ImageObject} image + */ + +/** + * set crop + * @name setCrop + * @methodOf Kinetic.Image.prototype + * @param {Object} config + */ + +/** + * set filter + * @name setFilter + * @methodOf Kinetic.Image.prototype + * @param {Object} config + */ + +/** + * get crop + * @name getCrop + * @methodOf Kinetic.Image.prototype + */ + +/** + * get image + * @name getImage + * @methodOf Kinetic.Image.prototype + */ + +/** + * get filter + * @name getFilter + * @methodOf Kinetic.Image.prototype + */ +/////////////////////////////////////////////////////////////////////// +// Polygon +/////////////////////////////////////////////////////////////////////// +/** + * Polygon constructor.  Polygons are defined by an array of points + * @constructor + * @augments Kinetic.Shape + * @param {Object} config + */ +Kinetic.Polygon = Kinetic.Shape.extend({ + init: function(config) { + this.setDefaultAttrs({ + points: [] + }); + + this.shapeType = "Polygon"; + config.drawFunc = this.drawFunc; + // call super constructor + this._super(config); + }, + drawFunc: function(context) { + context.beginPath(); + context.moveTo(this.attrs.points[0].x, this.attrs.points[0].y); + for(var n = 1; n < this.attrs.points.length; n++) { + context.lineTo(this.attrs.points[n].x, this.attrs.points[n].y); + } + context.closePath(); + this.fill(context); + this.stroke(context); + } +}); + +// add getters setters +Kinetic.Node.addGettersSetters(Kinetic.Polygon, ['points']); + +/** + * set points array + * @name setPoints + * @methodOf Kinetic.Polygon.prototype + * @param {Array} points can be an array of point objects or an array + * of Numbers. e.g. [{x:1,y:2},{x:3,y:4}] or [1,2,3,4] + */ + +/** + * get points array + * @name getPoints + * @methodOf Kinetic.Polygon.prototype + */ +/////////////////////////////////////////////////////////////////////// +// Text +/////////////////////////////////////////////////////////////////////// +/** + * Text constructor + * @constructor + * @augments Kinetic.Shape + * @param {Object} config + */ +Kinetic.Text = Kinetic.Shape.extend({ + init: function(config) { + this.setDefaultAttrs({ + fontFamily: 'Calibri', + text: '', + fontSize: 12, + align: 'left', + verticalAlign: 'top', + fontStyle: 'normal', + padding: 0, + width: 'auto', + height: 'auto', + detectionType: 'path', + cornerRadius: 0, + lineHeight: 1.2 + }); + + this.dummyCanvas = document.createElement('canvas'); + this.shapeType = "Text"; + + config.drawFunc = this.drawFunc; + // call super constructor + this._super(config); + + // update text data for certain attr changes + var attrs = ['fontFamily', 'fontSize', 'fontStyle', 'padding', 'align', 'lineHeight', 'text', 'width', 'height']; + var that = this; + for(var n = 0; n < attrs.length; n++) { + var attr = attrs[n]; + this.on(attr + 'Change.kinetic', that._setTextData); + } + that._setTextData(); + }, + drawFunc: function(context) { + // draw rect + context.beginPath(); + var boxWidth = this.getBoxWidth(); + var boxHeight = this.getBoxHeight(); + + if(this.attrs.cornerRadius === 0) { + // simple rect - don't bother doing all that complicated maths stuff. + context.rect(0, 0, boxWidth, boxHeight); + } + else { + // arcTo would be nicer, but browser support is patchy (Opera) + context.moveTo(this.attrs.cornerRadius, 0); + context.lineTo(boxWidth - this.attrs.cornerRadius, 0); + context.arc(boxWidth - this.attrs.cornerRadius, this.attrs.cornerRadius, this.attrs.cornerRadius, Math.PI * 3 / 2, 0, false); + context.lineTo(boxWidth, boxHeight - this.attrs.cornerRadius); + context.arc(boxWidth - this.attrs.cornerRadius, boxHeight - this.attrs.cornerRadius, this.attrs.cornerRadius, 0, Math.PI / 2, false); + context.lineTo(this.attrs.cornerRadius, boxHeight); + context.arc(this.attrs.cornerRadius, boxHeight - this.attrs.cornerRadius, this.attrs.cornerRadius, Math.PI / 2, Math.PI, false); + context.lineTo(0, this.attrs.cornerRadius); + context.arc(this.attrs.cornerRadius, this.attrs.cornerRadius, this.attrs.cornerRadius, Math.PI, Math.PI * 3 / 2, false); + } + context.closePath(); + + this.fill(context); + this.stroke(context); + /* + * draw text + */ + var p = this.attrs.padding; + var lineHeightPx = this.attrs.lineHeight * this.getTextHeight(); + var textArr = this.textArr; + + context.font = this.attrs.fontStyle + ' ' + this.attrs.fontSize + 'pt ' + this.attrs.fontFamily; + context.textBaseline = 'middle'; + context.textAlign = 'left'; + context.save(); + context.translate(p, 0); + context.translate(0, p + this.getTextHeight() / 2); + + // draw text lines + for(var n = 0; n < textArr.length; n++) { + var text = textArr[n]; + + // horizontal alignment + context.save(); + if(this.attrs.align === 'right') { + context.translate(this.getBoxWidth() - this._getTextSize(text).width - p * 2, 0); + } + else if(this.attrs.align === 'center') { + context.translate((this.getBoxWidth() - this._getTextSize(text).width - p * 2) / 2, 0); + } + + this.fillText(context, text); + this.strokeText(context, text); + context.restore(); + + context.translate(0, lineHeightPx); + } + context.restore(); + }, + /** + * get box width + * @name getBoxWidth + * @methodOf Kinetic.Text.prototype + */ + getBoxWidth: function() { + return this.attrs.width === 'auto' ? this.getTextWidth() + this.attrs.padding * 2 : this.attrs.width; + }, + /** + * get box height + * @name getBoxHeight + * @methodOf Kinetic.Text.prototype + */ + getBoxHeight: function() { + return this.attrs.height === 'auto' ? (this.getTextHeight() * this.textArr.length * this.attrs.lineHeight) + this.attrs.padding * 2 : this.attrs.height; + }, + /** + * get text width in pixels + * @name getTextWidth + * @methodOf Kinetic.Text.prototype + */ + getTextWidth: function() { + return this.textWidth; + }, + /** + * get text height in pixels + * @name getTextHeight + * @methodOf Kinetic.Text.prototype + */ + getTextHeight: function() { + return this.textHeight; + }, + _getTextSize: function(text) { + var dummyCanvas = this.dummyCanvas; + var context = dummyCanvas.getContext('2d'); + + context.save(); + context.font = this.attrs.fontStyle + ' ' + this.attrs.fontSize + 'pt ' + this.attrs.fontFamily; + var metrics = context.measureText(text); + context.restore(); + return { + width: metrics.width, + height: parseInt(this.attrs.fontSize, 10) + }; + }, + /** + * set text data. wrap logic and width and height setting occurs + * here + */ + _setTextData: function() { + var charArr = this.attrs.text.split(''); + var arr = []; + var row = 0; + var addLine = true; + this.textWidth = 0; + this.textHeight = this._getTextSize(this.attrs.text).height; + var lineHeightPx = this.attrs.lineHeight * this.textHeight; + while(charArr.length > 0 && addLine && (this.attrs.height === 'auto' || lineHeightPx * (row + 1) < this.attrs.height - this.attrs.padding * 2)) { + var index = 0; + var line = undefined; + addLine = false; + + while(index < charArr.length) { + if(charArr.indexOf('\n') === index) { + // remove newline char + charArr.splice(index, 1); + line = charArr.splice(0, index).join(''); + break; + } + + // if line exceeds inner box width + var lineArr = charArr.slice(0, index); + if(this.attrs.width !== 'auto' && this._getTextSize(lineArr.join('')).width > this.attrs.width - this.attrs.padding * 2) { + /* + * if a single character is too large to fit inside + * the text box width, then break out of the loop + * and stop processing + */ + if(index == 0) { + break; + } + var lastSpace = lineArr.lastIndexOf(' '); + var lastDash = lineArr.lastIndexOf('-'); + var wrapIndex = Math.max(lastSpace, lastDash); + if(wrapIndex >= 0) { + line = charArr.splice(0, 1 + wrapIndex).join(''); + break; + } + /* + * if not able to word wrap based on space or dash, + * go ahead and wrap in the middle of a word if needed + */ + line = charArr.splice(0, index).join(''); + break; + } + index++; + + // if the end is reached + if(index === charArr.length) { + line = charArr.splice(0, index).join(''); + } + } + this.textWidth = Math.max(this.textWidth, this._getTextSize(line).width); + if(line !== undefined) { + arr.push(line); + addLine = true; + } + row++; + } + this.textArr = arr; + } +}); +// add getters setters +Kinetic.Node.addGettersSetters(Kinetic.Text, ['fontFamily', 'fontSize', 'fontStyle', 'textFill', 'textStroke', 'textStrokeWidth', 'padding', 'align', 'lineHeight', 'text', 'width', 'height', 'cornerRadius', 'fill', 'stroke', 'strokeWidth', 'shadow']); + +/** + * set font family + * @name setFontFamily + * @methodOf Kinetic.Text.prototype + * @param {String} fontFamily + */ + +/** + * set font size + * @name setFontSize + * @methodOf Kinetic.Text.prototype + * @param {int} fontSize + */ + +/** + * set font style. Can be "normal", "italic", or "bold". "normal" is the default. + * @name setFontStyle + * @methodOf Kinetic.Text.prototype + * @param {String} fontStyle + */ + +/** + * set text fill color + * @name setTextFill + * @methodOf Kinetic.Text.prototype + * @param {String} textFill + */ + +/** + * set text stroke color + * @name setFontStroke + * @methodOf Kinetic.Text.prototype + * @param {String} textStroke + */ + +/** + * set text stroke width + * @name setTextStrokeWidth + * @methodOf Kinetic.Text.prototype + * @param {int} textStrokeWidth + */ + +/** + * set padding + * @name setPadding + * @methodOf Kinetic.Text.prototype + * @param {int} padding + */ + +/** + * set horizontal align of text + * @name setAlign + * @methodOf Kinetic.Text.prototype + * @param {String} align align can be 'left', 'center', or 'right' + */ + +/** + * set line height + * @name setLineHeight + * @methodOf Kinetic.Text.prototype + * @param {Number} lineHeight default is 1.2 + */ + +/** + * set text + * @name setText + * @methodOf Kinetic.Text.prototype + * @param {String} text + */ + +/** + * set width of text box + * @name setWidth + * @methodOf Kinetic.Text.prototype + * @param {Number} width + */ + +/** + * set height of text box + * @name setHeight + * @methodOf Kinetic.Text.prototype + * @param {Number} height + */ + +/** + * set shadow of text or textbox + * @name setShadow + * @methodOf Kinetic.Text.prototype + * @param {Object} config + */ + +/** + * get font family + * @name getFontFamily + * @methodOf Kinetic.Text.prototype + */ + +/** + * get font size + * @name getFontSize + * @methodOf Kinetic.Text.prototype + */ + +/** + * get font style + * @name getFontStyle + * @methodOf Kinetic.Text.prototype + */ + +/** + * get text fill color + * @name getTextFill + * @methodOf Kinetic.Text.prototype + */ + +/** + * get text stroke color + * @name getTextStroke + * @methodOf Kinetic.Text.prototype + */ + +/** + * get text stroke width + * @name getTextStrokeWidth + * @methodOf Kinetic.Text.prototype + */ + +/** + * get padding + * @name getPadding + * @methodOf Kinetic.Text.prototype + */ + +/** + * get horizontal align + * @name getAlign + * @methodOf Kinetic.Text.prototype + */ + +/** + * get line height + * @name getLineHeight + * @methodOf Kinetic.Text.prototype + */ + +/** + * get text + * @name getText + * @methodOf Kinetic.Text.prototype + */ + +/** + * get width of text box + * @name getWidth + * @methodOf Kinetic.Text.prototype + */ + +/** + * get height of text box + * @name getHeight + * @methodOf Kinetic.Text.prototype + */ + +/** + * get shadow of text or textbox + * @name getShadow + * @methodOf Kinetic.Text.prototype + */ +/////////////////////////////////////////////////////////////////////// +// Line +/////////////////////////////////////////////////////////////////////// +/** + * Line constructor.  Lines are defined by an array of points + * @constructor + * @augments Kinetic.Shape + * @param {Object} config + */ +Kinetic.Line = Kinetic.Shape.extend({ + init: function(config) { + this.setDefaultAttrs({ + points: [], + lineCap: 'butt', + dashArray: [], + detectionType: 'pixel' + }); + + this.shapeType = "Line"; + config.drawFunc = this.drawFunc; + // call super constructor + this._super(config); + }, + drawFunc: function(context) { + var lastPos = {}; + context.beginPath(); + + context.moveTo(this.attrs.points[0].x, this.attrs.points[0].y); + + for(var n = 1; n < this.attrs.points.length; n++) { + var x = this.attrs.points[n].x; + var y = this.attrs.points[n].y; + if(this.attrs.dashArray.length > 0) { + // draw dashed line + var lastX = this.attrs.points[n - 1].x; + var lastY = this.attrs.points[n - 1].y; + this._dashedLine(context, lastX, lastY, x, y, this.attrs.dashArray); + } + else { + // draw normal line + context.lineTo(x, y); + } + } + + if(!!this.attrs.lineCap) { + context.lineCap = this.attrs.lineCap; + } + + this.stroke(context); + }, + /** + * draw dashed line. Written by Phrogz + */ + _dashedLine: function(context, x, y, x2, y2, dashArray) { + var dashCount = dashArray.length; + + var dx = (x2 - x), dy = (y2 - y); + var xSlope = dx > dy; + var slope = (xSlope) ? dy / dx : dx / dy; + + /* + * gaurd against slopes of infinity + */ + if(slope > 9999) { + slope = 9999; + } + else if(slope < -9999) { + slope = -9999; + } + + var distRemaining = Math.sqrt(dx * dx + dy * dy); + var dashIndex = 0, draw = true; + while(distRemaining >= 0.1 && dashIndex < 10000) { + var dashLength = dashArray[dashIndex++ % dashCount]; + if(dashLength === 0) { + dashLength = 0.001; + } + if(dashLength > distRemaining) { + dashLength = distRemaining; + } + var step = Math.sqrt(dashLength * dashLength / (1 + slope * slope)); + if(xSlope) { + x += dx < 0 && dy < 0 ? step * -1 : step; + y += dx < 0 && dy < 0 ? slope * step * -1 : slope * step; + } + else { + x += dx < 0 && dy < 0 ? slope * step * -1 : slope * step; + y += dx < 0 && dy < 0 ? step * -1 : step; + } + context[draw ? 'lineTo' : 'moveTo'](x, y); + distRemaining -= dashLength; + draw = !draw; + } + + context.moveTo(x2, y2); + } +}); + +// add getters setters +Kinetic.Node.addGettersSetters(Kinetic.Line, ['dashArray', 'lineCap', 'points']); + +/** + * set dash array. + * @name setDashArray + * @methodOf Kinetic.Line.prototype + * @param {Array} dashArray + * examples:
+ * [10, 5] dashes are 10px long and 5 pixels apart + * [10, 20, 0, 20] if using a round lineCap, the line will + * be made up of alternating dashed lines that are 10px long + * and 20px apart, and dots that have a radius of 5 and are 20px + * apart + */ + +/** + * set line cap. Can be butt, round, or square + * @name setLineCap + * @methodOf Kinetic.Line.prototype + * @param {String} lineCap + */ + +/** + * set points array + * @name setPoints + * @methodOf Kinetic.Line.prototype + * @param {Array} can be an array of point objects or an array + * of Numbers. e.g. [{x:1,y:2},{x:3,y:4}] or [1,2,3,4] + */ + +/** + * get dash array + * @name getDashArray + * @methodOf Kinetic.Line.prototype + */ + +/** + * get line cap + * @name getLineCap + * @methodOf Kinetic.Line.prototype + */ + +/** + * get points array + * @name getPoints + * @methodOf Kinetic.Line.prototype + */ +/////////////////////////////////////////////////////////////////////// +// Sprite +/////////////////////////////////////////////////////////////////////// +/** + * Sprite constructor + * @constructor + * @augments Kinetic.Shape + * @param {Object} config + */ +Kinetic.Sprite = Kinetic.Shape.extend({ + init: function(config) { + this.setDefaultAttrs({ + index: 0, + frameRate: 17 + }); + + config.drawFunc = this.drawFunc; + // call super constructor + this._super(config); + this.anim = new Kinetic.Animation(); + var that = this; + this.on('animationChange.kinetic', function() { + // reset index when animation changes + that.setIndex(0); + }); + }, + drawFunc: function(context) { + if(this.attrs.image) { + var anim = this.attrs.animation; + var index = this.attrs.index; + var f = this.attrs.animations[anim][index]; + + context.beginPath(); + context.rect(0, 0, f.width, f.height); + context.closePath(); + + this.drawImage(context, this.attrs.image, f.x, f.y, f.width, f.height, 0, 0, f.width, f.height); + } + }, + /** + * start sprite animation + * @name start + * @methodOf Kinetic.Sprite.prototype + */ + start: function() { + var that = this; + var layer = this.getLayer(); + var ka = Kinetic.Animation; + + // if sprite already has an animation, remove it + ka._removeAnimation(this.anim); + + /* + * animation object has no executable function because + * the updates are done with a fixed FPS with the setInterval + * below. The anim object only needs the layer reference for + * redraw + */ + this.anim.node = layer; + + /* + * adding the animation with the addAnimation + * method auto generates an id + */ + ka._addAnimation(this.anim); + + this.interval = setInterval(function() { + var index = that.attrs.index; + that._updateIndex(); + if(that.afterFrameFunc && index === that.afterFrameIndex) { + that.afterFrameFunc(); + } + }, 1000 / this.attrs.frameRate); + + ka._handleAnimation(); + }, + /** + * stop sprite animation + * @name stop + * @methodOf Kinetic.Sprite.prototype + */ + stop: function() { + Kinetic.Animation._removeAnimation(this.anim); + clearInterval(this.interval); + }, + /** + * set after frame event handler + * @name afterFrame + * @methodOf Kinetic.Sprite.prototype + * @param {Integer} index frame index + * @param {Function} func function to be executed after frame has been drawn + */ + afterFrame: function(index, func) { + this.afterFrameIndex = index; + this.afterFrameFunc = func; + }, + _updateIndex: function() { + var i = this.attrs.index; + var a = this.attrs.animation; + if(i < this.attrs.animations[a].length - 1) { + this.attrs.index++; + } + else { + this.attrs.index = 0; + } + } +}); + +// add getters setters +Kinetic.Node.addGettersSetters(Kinetic.Sprite, ['animation', 'animations', 'index']); + +/** + * set animation key + * @name setAnimation + * @methodOf Kinetic.Sprite.prototype + * @param {String} anim animation key + */ + +/** + * set animations obect + * @name setAnimations + * @methodOf Kinetic.Sprite.prototype + * @param {Object} animations + */ + +/** + * set animation frame index + * @name setIndex + * @methodOf Kinetic.Sprite.prototype + * @param {Integer} index frame index + */ + +/** + * get animation key + * @name getAnimation + * @methodOf Kinetic.Sprite.prototype + */ + +/** + * get animations object + * @name getAnimations + * @methodOf Kinetic.Sprite.prototype + */ + +/** + * get animation frame index + * @name getIndex + * @methodOf Kinetic.Sprite.prototype + */ +/////////////////////////////////////////////////////////////////////// +// Star +/////////////////////////////////////////////////////////////////////// +/** + * Star constructor + * @constructor + * @augments Kinetic.Shape + * @param {Object} config + */ +Kinetic.Star = Kinetic.Shape.extend({ + init: function(config) { + this.setDefaultAttrs({ + numPoints: 0, + innerRadius: 0, + outerRadius: 0 + }); + + this.shapeType = "Star"; + config.drawFunc = this.drawFunc; + // call super constructor + this._super(config); + }, + drawFunc: function(context) { + context.beginPath(); + context.moveTo(0, 0 - this.attrs.outerRadius); + + for(var n = 1; n < this.attrs.numPoints * 2; n++) { + var radius = n % 2 === 0 ? this.attrs.outerRadius : this.attrs.innerRadius; + var x = radius * Math.sin(n * Math.PI / this.attrs.numPoints); + var y = -1 * radius * Math.cos(n * Math.PI / this.attrs.numPoints); + context.lineTo(x, y); + } + context.closePath(); + + this.fill(context); + this.stroke(context); + } +}); + +// add getters setters +Kinetic.Node.addGettersSetters(Kinetic.Star, ['numPoints', 'innerRadius', 'outerRadius']); + +/** + * set number of points + * @name setNumPoints + * @methodOf Kinetic.Star.prototype + * @param {Integer} points + */ + +/** + * set outer radius + * @name setOuterRadius + * @methodOf Kinetic.Star.prototype + * @param {Number} radius + */ + +/** + * set inner radius + * @name setInnerRadius + * @methodOf Kinetic.Star.prototype + * @param {Number} radius + */ + +/** + * get number of points + * @name getNumPoints + * @methodOf Kinetic.Star.prototype + */ + +/** + * get outer radius + * @name getOuterRadius + * @methodOf Kinetic.Star.prototype + */ + +/** + * get inner radius + * @name getInnerRadius + * @methodOf Kinetic.Star.prototype + */ +/////////////////////////////////////////////////////////////////////// +// RegularPolygon +/////////////////////////////////////////////////////////////////////// +/** + * RegularPolygon constructor.  Examples include triangles, squares, pentagons, hexagons, etc. + * @constructor + * @augments Kinetic.Shape + * @param {Object} config + */ +Kinetic.RegularPolygon = Kinetic.Shape.extend({ + init: function(config) { + this.setDefaultAttrs({ + radius: 0, + sides: 0 + }); + + this.shapeType = "RegularPolygon"; + config.drawFunc = this.drawFunc; + // call super constructor + this._super(config); + }, + drawFunc: function(context) { + context.beginPath(); + context.moveTo(0, 0 - this.attrs.radius); + + for(var n = 1; n < this.attrs.sides; n++) { + var x = this.attrs.radius * Math.sin(n * 2 * Math.PI / this.attrs.sides); + var y = -1 * this.attrs.radius * Math.cos(n * 2 * Math.PI / this.attrs.sides); + context.lineTo(x, y); + } + context.closePath(); + this.fill(context); + this.stroke(context); + } +}); + +// add getters setters +Kinetic.Node.addGettersSetters(Kinetic.RegularPolygon, ['radius', 'sides']); + +/** + * set radius + * @name setRadius + * @methodOf Kinetic.RegularPolygon.prototype + * @param {Number} radius + */ + +/** + * set number of sides + * @name setSides + * @methodOf Kinetic.RegularPolygon.prototype + * @param {int} sides + */ +/** + * get radius + * @name getRadius + * @methodOf Kinetic.RegularPolygon.prototype + */ + +/** + * get number of sides + * @name getSides + * @methodOf Kinetic.RegularPolygon.prototype + */ +/////////////////////////////////////////////////////////////////////// +// SVG Path +/////////////////////////////////////////////////////////////////////// +/** + * Path constructor. + * @author Jason Follas + * @constructor + * @augments Kinetic.Shape + * @param {Object} config + */ +Kinetic.Path = Kinetic.Shape.extend({ + init: function(config) { + this.shapeType = "Path"; + this.dataArray = []; + var that = this; + + config.drawFunc = this.drawFunc; + // call super constructor + this._super(config); + this.dataArray = Kinetic.Path.parsePathData(this.attrs.data); + this.on('dataChange', function() { + that.dataArray = Kinetic.Path.parsePathData(that.attrs.data); + }); + }, + drawFunc: function(context) { + var ca = this.dataArray; + // context position + context.beginPath(); + for(var n = 0; n < ca.length; n++) { + var c = ca[n].command; + var p = ca[n].points; + switch (c) { + case 'L': + context.lineTo(p[0], p[1]); + break; + case 'M': + context.moveTo(p[0], p[1]); + break; + case 'C': + context.bezierCurveTo(p[0], p[1], p[2], p[3], p[4], p[5]); + break; + case 'Q': + context.quadraticCurveTo(p[0], p[1], p[2], p[3]); + break; + case 'A': + var cx = p[0], cy = p[1], rx = p[2], ry = p[3], theta = p[4], dTheta = p[5], psi = p[6], fs = p[7]; + + var r = (rx > ry) ? rx : ry; + var scaleX = (rx > ry) ? 1 : rx / ry; + var scaleY = (rx > ry) ? ry / rx : 1; + + context.translate(cx, cy); + context.rotate(psi); + context.scale(scaleX, scaleY); + context.arc(0, 0, r, theta, theta + dTheta, 1 - fs); + context.scale(1 / scaleX, 1 / scaleY); + context.rotate(-psi); + context.translate(-cx, -cy); + + break; + case 'z': + context.closePath(); + break; + } + } + this.fill(context); + this.stroke(context); + } +}); + +/* + * Utility methods written by jfollas to + * handle length and point measurements + */ +Kinetic.Path.getLineLength = function(x1, y1, x2, y2) { + return Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)); +}; +Kinetic.Path.getPointOnLine = function(dist, P1x, P1y, P2x, P2y, fromX, fromY) { + if(fromX === undefined) { + fromX = P1x; + } + if(fromY === undefined) { + fromY = P1y; + } + + var m = (P2y - P1y) / ((P2x - P1x) + 0.00000001); + var run = Math.sqrt(dist * dist / (1 + m * m)); + var rise = m * run; + var pt; + + if((fromY - P1y) / ((fromX - P1x) + 0.00000001) === m) { + pt = { + x: fromX + run, + y: fromY + rise + }; + } + else { + var ix, iy; + + var len = this.getLineLength(P1x, P1y, P2x, P2y); + if(len < 0.00000001) { + return undefined; + } + var u = (((fromX - P1x) * (P2x - P1x)) + ((fromY - P1y) * (P2y - P1y))); + u = u / (len * len); + ix = P1x + u * (P2x - P1x); + iy = P1y + u * (P2y - P1y); + + var pRise = this.getLineLength(fromX, fromY, ix, iy); + var pRun = Math.sqrt(dist * dist - pRise * pRise); + run = Math.sqrt(pRun * pRun / (1 + m * m)); + rise = m * run; + pt = { + x: ix + run, + y: iy + rise + }; + } + + return pt; +}; + +Kinetic.Path.getPointOnCubicBezier = function(pct, P1x, P1y, P2x, P2y, P3x, P3y, P4x, P4y) { + function CB1(t) { + return t * t * t; + } + function CB2(t) { + return 3 * t * t * (1 - t); + } + function CB3(t) { + return 3 * t * (1 - t) * (1 - t); + } + function CB4(t) { + return (1 - t) * (1 - t) * (1 - t); + } + var x = P4x * CB1(pct) + P3x * CB2(pct) + P2x * CB3(pct) + P1x * CB4(pct); + var y = P4y * CB1(pct) + P3y * CB2(pct) + P2y * CB3(pct) + P1y * CB4(pct); + + return { + x: x, + y: y + }; +}; +Kinetic.Path.getPointOnQuadraticBezier = function(pct, P1x, P1y, P2x, P2y, P3x, P3y) { + function QB1(t) { + return t * t; + } + function QB2(t) { + return 2 * t * (1 - t); + } + function QB3(t) { + return (1 - t) * (1 - t); + } + var x = P3x * QB1(pct) + P2x * QB2(pct) + P1x * QB3(pct); + var y = P3y * QB1(pct) + P2y * QB2(pct) + P1y * QB3(pct); + + return { + x: x, + y: y + }; +}; +Kinetic.Path.getPointOnEllipticalArc = function(cx, cy, rx, ry, theta, psi) { + var cosPsi = Math.cos(psi), sinPsi = Math.sin(psi); + var pt = { + x: rx * Math.cos(theta), + y: ry * Math.sin(theta) + }; + return { + x: cx + (pt.x * cosPsi - pt.y * sinPsi), + y: cy + (pt.x * sinPsi + pt.y * cosPsi) + }; +}; +/** + * get parsed data array from the data + * string. V, v, H, h, and l data are converted to + * L data for the purpose of high performance Path + * rendering + */ +Kinetic.Path.parsePathData = function(data) { + // Path Data Segment must begin with a moveTo + //m (x y)+ Relative moveTo (subsequent points are treated as lineTo) + //M (x y)+ Absolute moveTo (subsequent points are treated as lineTo) + //l (x y)+ Relative lineTo + //L (x y)+ Absolute LineTo + //h (x)+ Relative horizontal lineTo + //H (x)+ Absolute horizontal lineTo + //v (y)+ Relative vertical lineTo + //V (y)+ Absolute vertical lineTo + //z (closepath) + //Z (closepath) + //c (x1 y1 x2 y2 x y)+ Relative Bezier curve + //C (x1 y1 x2 y2 x y)+ Absolute Bezier curve + //q (x1 y1 x y)+ Relative Quadratic Bezier + //Q (x1 y1 x y)+ Absolute Quadratic Bezier + //t (x y)+ Shorthand/Smooth Relative Quadratic Bezier + //T (x y)+ Shorthand/Smooth Absolute Quadratic Bezier + //s (x2 y2 x y)+ Shorthand/Smooth Relative Bezier curve + //S (x2 y2 x y)+ Shorthand/Smooth Absolute Bezier curve + //a (rx ry x-axis-rotation large-arc-flag sweep-flag x y)+ Relative Elliptical Arc + //A (rx ry x-axis-rotation large-arc-flag sweep-flag x y)+ Absolute Elliptical Arc + + // return early if data is not defined + if(!data) { + return []; + } + + // command string + var cs = data; + + // command chars + var cc = ['m', 'M', 'l', 'L', 'v', 'V', 'h', 'H', 'z', 'Z', 'c', 'C', 'q', 'Q', 't', 'T', 's', 'S', 'a', 'A']; + // convert white spaces to commas + cs = cs.replace(new RegExp(' ', 'g'), ','); + // create pipes so that we can split the data + for(var n = 0; n < cc.length; n++) { + cs = cs.replace(new RegExp(cc[n], 'g'), '|' + cc[n]); + } + // create array + var arr = cs.split('|'); + var ca = []; + // init context point + var cpx = 0; + var cpy = 0; + for(var n = 1; n < arr.length; n++) { + var str = arr[n]; + var c = str.charAt(0); + str = str.slice(1); + // remove ,- for consistency + str = str.replace(new RegExp(',-', 'g'), '-'); + // add commas so that it's easy to split + str = str.replace(new RegExp('-', 'g'), ',-'); + str = str.replace(new RegExp('e,-', 'g'), 'e-'); + var p = str.split(','); + if(p.length > 0 && p[0] === '') { + p.shift(); + } + // convert strings to floats + for(var i = 0; i < p.length; i++) { + p[i] = parseFloat(p[i]); + } + while(p.length > 0) { + if(isNaN(p[0]))// case for a trailing comma before next command + break; + + var cmd = null; + var points = []; + var startX = cpx, startY = cpy; + + // convert l, H, h, V, and v to L + switch (c) { + + // Note: Keep the lineTo's above the moveTo's in this switch + case 'l': + cpx += p.shift(); + cpy += p.shift(); + cmd = 'L'; + points.push(cpx, cpy); + break; + case 'L': + cpx = p.shift(); + cpy = p.shift(); + points.push(cpx, cpy); + break; + + // Note: lineTo handlers need to be above this point + case 'm': + cpx += p.shift(); + cpy += p.shift(); + cmd = 'M'; + points.push(cpx, cpy); + c = 'l'; + // subsequent points are treated as relative lineTo + break; + case 'M': + cpx = p.shift(); + cpy = p.shift(); + cmd = 'M'; + points.push(cpx, cpy); + c = 'L'; + // subsequent points are treated as absolute lineTo + break; + + case 'h': + cpx += p.shift(); + cmd = 'L'; + points.push(cpx, cpy); + break; + case 'H': + cpx = p.shift(); + cmd = 'L'; + points.push(cpx, cpy); + break; + case 'v': + cpy += p.shift(); + cmd = 'L'; + points.push(cpx, cpy); + break; + case 'V': + cpy = p.shift(); + cmd = 'L'; + points.push(cpx, cpy); + break; + case 'C': + points.push(p.shift(), p.shift(), p.shift(), p.shift()); + cpx = p.shift(); + cpy = p.shift(); + points.push(cpx, cpy); + break; + case 'c': + points.push(cpx + p.shift(), cpy + p.shift(), cpx + p.shift(), cpy + p.shift()); + cpx += p.shift(); + cpy += p.shift(); + cmd = 'C'; + points.push(cpx, cpy); + break; + case 'S': + var ctlPtx = cpx, ctlPty = cpy; + var prevCmd = ca[ca.length - 1]; + if(prevCmd.command === 'C') { + ctlPtx = cpx + (cpx - prevCmd.points[2]); + ctlPty = cpy + (cpy - prevCmd.points[3]); + } + points.push(ctlPtx, ctlPty, p.shift(), p.shift()); + cpx = p.shift(); + cpy = p.shift(); + cmd = 'C'; + points.push(cpx, cpy); + break; + case 's': + var ctlPtx = cpx, ctlPty = cpy; + var prevCmd = ca[ca.length - 1]; + if(prevCmd.command === 'C') { + ctlPtx = cpx + (cpx - prevCmd.points[2]); + ctlPty = cpy + (cpy - prevCmd.points[3]); + } + points.push(ctlPtx, ctlPty, cpx + p.shift(), cpy + p.shift()); + cpx += p.shift(); + cpy += p.shift(); + cmd = 'C'; + points.push(cpx, cpy); + break; + case 'Q': + points.push(p.shift(), p.shift()); + cpx = p.shift(); + cpy = p.shift(); + points.push(cpx, cpy); + break; + case 'q': + points.push(cpx + p.shift(), cpy + p.shift()); + cpx += p.shift(); + cpy += p.shift(); + cmd = 'Q'; + points.push(cpx, cpy); + break; + case 'T': + var ctlPtx = cpx, ctlPty = cpy; + var prevCmd = ca[ca.length - 1]; + if(prevCmd.command === 'Q') { + ctlPtx = cpx + (cpx - prevCmd.points[0]); + ctlPty = cpy + (cpy - prevCmd.points[1]); + } + cpx = p.shift(); + cpy = p.shift(); + cmd = 'Q'; + points.push(ctlPtx, ctlPty, cpx, cpy); + break; + case 't': + var ctlPtx = cpx, ctlPty = cpy; + var prevCmd = ca[ca.length - 1]; + if(prevCmd.command === 'Q') { + ctlPtx = cpx + (cpx - prevCmd.points[0]); + ctlPty = cpy + (cpy - prevCmd.points[1]); + } + cpx += p.shift(); + cpy += p.shift(); + cmd = 'Q'; + points.push(ctlPtx, ctlPty, cpx, cpy); + break; + case 'A': + var rx = p.shift(), ry = p.shift(), psi = p.shift(), fa = p.shift(), fs = p.shift(); + var x1 = cpx, y1 = cpy; + cpx = p.shift(), cpy = p.shift(); + cmd = 'A'; + points = this.convertEndpointToCenterParameterization(x1, y1, cpx, cpy, fa, fs, rx, ry, psi); + break; + case 'a': + var rx = p.shift(), ry = p.shift(), psi = p.shift(), fa = p.shift(), fs = p.shift(); + var x1 = cpx, y1 = cpy; + cpx += p.shift(), cpy += p.shift(); + cmd = 'A'; + points = this.convertEndpointToCenterParameterization(x1, y1, cpx, cpy, fa, fs, rx, ry, psi); + break; + } + + ca.push({ + command: cmd || c, + points: points, + start: { + x: startX, + y: startY + }, + pathLength: this.calcLength(startX, startY, cmd || c, points) + }); + } + + if(c === 'z' || c === 'Z') { + ca.push({ + command: 'z', + points: [], + start: undefined, + pathLength: 0 + }); + } + } + + return ca; +}; +Kinetic.Path.calcLength = function(x, y, cmd, points) { + var len, p1, p2; + var path = Kinetic.Path; + + switch (cmd) { + case 'L': + return path.getLineLength(x, y, points[0], points[1]); + case 'C': + // Approximates by breaking curve into 100 line segments + len = 0.0; + p1 = path.getPointOnCubicBezier(0, x, y, points[0], points[1], points[2], points[3], points[4], points[5]); + for( t = 0.01; t <= 1; t += 0.01) { + p2 = path.getPointOnCubicBezier(t, x, y, points[0], points[1], points[2], points[3], points[4], points[5]); + len += path.getLineLength(p1.x, p1.y, p2.x, p2.y); + p1 = p2; + } + return len; + case 'Q': + // Approximates by breaking curve into 100 line segments + len = 0.0; + p1 = path.getPointOnQuadraticBezier(0, x, y, points[0], points[1], points[2], points[3]); + for( t = 0.01; t <= 1; t += 0.01) { + p2 = path.getPointOnQuadraticBezier(t, x, y, points[0], points[1], points[2], points[3]); + len += path.getLineLength(p1.x, p1.y, p2.x, p2.y); + p1 = p2; + } + return len; + case 'A': + // Approximates by breaking curve into line segments + len = 0.0; + var start = points[4]; + // 4 = theta + var dTheta = points[5]; + // 5 = dTheta + var end = points[4] + dTheta; + var inc = Math.PI / 180.0; + // 1 degree resolution + if(Math.abs(start - end) < inc) { + inc = Math.abs(start - end); + } + // Note: for purpose of calculating arc length, not going to worry about rotating X-axis by angle psi + p1 = path.getPointOnEllipticalArc(points[0], points[1], points[2], points[3], start, 0); + if(dTheta < 0) {// clockwise + for( t = start - inc; t > end; t -= inc) { + p2 = path.getPointOnEllipticalArc(points[0], points[1], points[2], points[3], t, 0); + len += path.getLineLength(p1.x, p1.y, p2.x, p2.y); + p1 = p2; + } + } + else {// counter-clockwise + for( t = start + inc; t < end; t += inc) { + p2 = path.getPointOnEllipticalArc(points[0], points[1], points[2], points[3], t, 0); + len += path.getLineLength(p1.x, p1.y, p2.x, p2.y); + p1 = p2; + } + } + p2 = path.getPointOnEllipticalArc(points[0], points[1], points[2], points[3], end, 0); + len += path.getLineLength(p1.x, p1.y, p2.x, p2.y); + + return len; + } + + return 0; +}; +Kinetic.Path.convertEndpointToCenterParameterization = function(x1, y1, x2, y2, fa, fs, rx, ry, psiDeg) { + // Derived from: http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes + var psi = psiDeg * (Math.PI / 180.0); + var xp = Math.cos(psi) * (x1 - x2) / 2.0 + Math.sin(psi) * (y1 - y2) / 2.0; + var yp = -1 * Math.sin(psi) * (x1 - x2) / 2.0 + Math.cos(psi) * (y1 - y2) / 2.0; + + var lambda = (xp * xp) / (rx * rx) + (yp * yp) / (ry * ry); + + if(lambda > 1) { + rx *= Math.sqrt(lambda); + ry *= Math.sqrt(lambda); + } + + var f = Math.sqrt((((rx * rx) * (ry * ry)) - ((rx * rx) * (yp * yp)) - ((ry * ry) * (xp * xp))) / ((rx * rx) * (yp * yp) + (ry * ry) * (xp * xp))); + + if(fa == fs) { + f *= -1; + } + if(isNaN(f)) { + f = 0; + } + + var cxp = f * rx * yp / ry; + var cyp = f * -ry * xp / rx; + + var cx = (x1 + x2) / 2.0 + Math.cos(psi) * cxp - Math.sin(psi) * cyp; + var cy = (y1 + y2) / 2.0 + Math.sin(psi) * cxp + Math.cos(psi) * cyp; + + var vMag = function(v) { + return Math.sqrt(v[0] * v[0] + v[1] * v[1]); + }; + var vRatio = function(u, v) { + return (u[0] * v[0] + u[1] * v[1]) / (vMag(u) * vMag(v)); + }; + var vAngle = function(u, v) { + return (u[0] * v[1] < u[1] * v[0] ? -1 : 1) * Math.acos(vRatio(u, v)); + }; + var theta = vAngle([1, 0], [(xp - cxp) / rx, (yp - cyp) / ry]); + var u = [(xp - cxp) / rx, (yp - cyp) / ry]; + var v = [(-1 * xp - cxp) / rx, (-1 * yp - cyp) / ry]; + var dTheta = vAngle(u, v); + + if(vRatio(u, v) <= -1) { + dTheta = Math.PI; + } + if(vRatio(u, v) >= 1) { + dTheta = 0; + } + if(fs === 0 && dTheta > 0) { + dTheta = dTheta - 2 * Math.PI; + } + if(fs == 1 && dTheta < 0) { + dTheta = dTheta + 2 * Math.PI; + } + return [cx, cy, rx, ry, theta, dTheta, psi, fs]; +}; + +// add getters setters +Kinetic.Node.addGettersSetters(Kinetic.Path, ['data']); + +/** + * set SVG path data string. This method + * also automatically parses the data string + * into a data array. Currently supported SVG data: + * M, m, L, l, H, h, V, v, Q, q, T, t, C, c, S, s, A, a, Z, z + * @name setData + * @methodOf Kinetic.Path.prototype + * @param {String} SVG path command string + */ + +/** + * get SVG path data string + * @name getData + * @methodOf Kinetic.Path.prototype + */ +/////////////////////////////////////////////////////////////////////// +// Text Path +/////////////////////////////////////////////////////////////////////// +/** + * Path constructor. + * @author Jason Follas + * @constructor + * @augments Kinetic.Shape + * @param {Object} config + */ +Kinetic.TextPath = Kinetic.Shape.extend({ + init: function(config) { + this.setDefaultAttrs({ + fontFamily: 'Calibri', + fontSize: 12, + fontStyle: 'normal', + detectionType: 'path', + text: '' + }); + + this.dummyCanvas = document.createElement('canvas'); + this.shapeType = "TextPath"; + this.dataArray = []; + var that = this; + + config.drawFunc = this.drawFunc; + // call super constructor + this._super(config); + this.dataArray = Kinetic.Path.parsePathData(this.attrs.data); + this.on('dataChange', function() { + that.dataArray = Kinetic.Path.parsePathData(this.attrs.data); + }); + // update text data for certain attr changes + var attrs = ['text', 'textStroke', 'textStrokeWidth']; + for(var n = 0; n < attrs.length; n++) { + var attr = attrs[n]; + this.on(attr + 'Change', that._setTextData); + } + that._setTextData(); + }, + drawFunc: function(context) { + var charArr = this.charArr; + + context.font = this.attrs.fontStyle + ' ' + this.attrs.fontSize + 'pt ' + this.attrs.fontFamily; + context.textBaseline = 'middle'; + context.textAlign = 'left'; + context.save(); + + var glyphInfo = this.glyphInfo; + + for(var i = 0; i < glyphInfo.length; i++) { + context.save(); + + var p0 = glyphInfo[i].p0; + var p1 = glyphInfo[i].p1; + var ht = parseFloat(this.attrs.fontSize); + + context.translate(p0.x, p0.y); + + context.rotate(glyphInfo[i].rotation); + + this.fillText(context, glyphInfo[i].text); + this.strokeText(context, glyphInfo[i].text); + + context.restore(); + + //// To assist with debugging visually, uncomment following + // context.beginPath(); + // if (i % 2) + // context.strokeStyle = 'cyan'; + // else + // context.strokeStyle = 'green'; + + // context.moveTo(p0.x, p0.y); + // context.lineTo(p1.x, p1.y); + // context.stroke(); + } + + context.restore(); + }, + /** + * get text width in pixels + * @name getTextWidth + * @methodOf Kinetic.TextPath.prototype + */ + getTextWidth: function() { + return this.textWidth; + }, + /** + * get text height in pixels + * @name getTextHeight + * @methodOf Kinetic.TextPath.prototype + */ + getTextHeight: function() { + return this.textHeight; + }, + _getTextSize: function(text) { + var dummyCanvas = this.dummyCanvas; + var context = dummyCanvas.getContext('2d'); + + context.save(); + + context.font = this.attrs.fontStyle + ' ' + this.attrs.fontSize + 'pt ' + this.attrs.fontFamily; + var metrics = context.measureText(text); + + context.restore(); + + return { + width: metrics.width, + height: parseInt(this.attrs.fontSize, 10) + }; + }, + /** + * set text data. + */ + _setTextData: function() { + + var that = this; + var size = this._getTextSize(this.attrs.text); + this.textWidth = size.width; + this.textHeight = size.height; + + this.glyphInfo = []; + + var charArr = this.attrs.text.split(''); + + var p0, p1, pathCmd; + + var pIndex = -1; + var currentT = 0; + + var getNextPathSegment = function() { + currentT = 0; + var pathData = that.dataArray; + + for(var i = pIndex + 1; i < pathData.length; i++) { + if(pathData[i].pathLength > 0) { + pIndex = i; + + return pathData[i]; + } + else if(pathData[i].command == 'M') { + p0 = { + x: pathData[i].points[0], + y: pathData[i].points[1] + }; + } + } + + return {}; + }; + var findSegmentToFitCharacter = function(c, before) { + + var glyphWidth = that._getTextSize(c).width; + + var currLen = 0; + var attempts = 0; + var needNextSegment = false; + p1 = undefined; + while(Math.abs(glyphWidth - currLen) / glyphWidth > 0.01 && attempts < 25) { + attempts++; + var cumulativePathLength = currLen; + while(pathCmd === undefined) { + pathCmd = getNextPathSegment(); + + if(pathCmd && cumulativePathLength + pathCmd.pathLength < glyphWidth) { + cumulativePathLength += pathCmd.pathLength; + pathCmd = undefined; + } + } + + if(pathCmd === {} || p0 === undefined) + return undefined; + + var needNewSegment = false; + + switch (pathCmd.command) { + case 'L': + if(Kinetic.Path.getLineLength(p0.x, p0.y, pathCmd.points[0], pathCmd.points[1]) > glyphWidth) { + p1 = Kinetic.Path.getPointOnLine(glyphWidth, p0.x, p0.y, pathCmd.points[0], pathCmd.points[1], p0.x, p0.y); + } + else + pathCmd = undefined; + break; + case 'A': + + var start = pathCmd.points[4]; + // 4 = theta + var dTheta = pathCmd.points[5]; + // 5 = dTheta + var end = pathCmd.points[4] + dTheta; + + if(currentT === 0) + currentT = start + 0.00000001; + // Just in case start is 0 + else if(glyphWidth > currLen) + currentT += (Math.PI / 180.0) * dTheta / Math.abs(dTheta); + else + currentT -= Math.PI / 360.0 * dTheta / Math.abs(dTheta); + + if(Math.abs(currentT) > Math.abs(end)) { + currentT = end; + needNewSegment = true; + } + p1 = Kinetic.Path.getPointOnEllipticalArc(pathCmd.points[0], pathCmd.points[1], pathCmd.points[2], pathCmd.points[3], currentT, pathCmd.points[6]); + break; + case 'C': + if(currentT === 0) { + if(glyphWidth > pathCmd.pathLength) + currentT = 0.00000001; + else + currentT = glyphWidth / pathCmd.pathLength; + } + else if(glyphWidth > currLen) + currentT += (glyphWidth - currLen) / pathCmd.pathLength; + else + currentT -= (currLen - glyphWidth) / pathCmd.pathLength; + + if(currentT > 1.0) { + currentT = 1.0; + needNewSegment = true; + } + p1 = Kinetic.Path.getPointOnCubicBezier(currentT, pathCmd.start.x, pathCmd.start.y, pathCmd.points[0], pathCmd.points[1], pathCmd.points[2], pathCmd.points[3], pathCmd.points[4], pathCmd.points[5]); + break; + case 'Q': + if(currentT === 0) + currentT = glyphWidth / pathCmd.pathLength; + else if(glyphWidth > currLen) + currentT += (glyphWidth - currLen) / pathCmd.pathLength; + else + currentT -= (currLen - glyphWidth) / pathCmd.pathLength; + + if(currentT > 1.0) { + currentT = 1.0; + needNewSegment = true; + } + p1 = Kinetic.Path.getPointOnQuadraticBezier(currentT, pathCmd.start.x, pathCmd.start.y, pathCmd.points[0], pathCmd.points[1], pathCmd.points[2], pathCmd.points[3]); + break; + + } + + if(p1 !== undefined) { + currLen = Kinetic.Path.getLineLength(p0.x, p0.y, p1.x, p1.y); + } + + if(needNewSegment) { + needNewSegment = false; + pathCmd = undefined; + } + } + }; + for(var i = 0; i < charArr.length; i++) { + + // Find p1 such that line segment between p0 and p1 is approx. width of glyph + findSegmentToFitCharacter(charArr[i]); + + if(p0 === undefined || p1 === undefined) + break; + + var width = Kinetic.Path.getLineLength(p0.x, p0.y, p1.x, p1.y); + + // Note: Since glyphs are rendered one at a time, any kerning pair data built into the font will not be used. + // Can foresee having a rough pair table built in that the developer can override as needed. + + var kern = 0; + // placeholder for future implementation + + var midpoint = Kinetic.Path.getPointOnLine(kern + width / 2.0, p0.x, p0.y, p1.x, p1.y); + + var rotation = Math.atan2((p1.y - p0.y), (p1.x - p0.x)); + this.glyphInfo.push({ + transposeX: midpoint.x, + transposeY: midpoint.y, + text: charArr[i], + rotation: rotation, + p0: p0, + p1: p1 + }); + p0 = p1; + } + } +}); + +// add setters and getters +Kinetic.Node.addGettersSetters(Kinetic.TextPath, ['fontFamily', 'fontSize', 'fontStyle', 'textFill', 'textStroke', 'textStrokeWidth', 'text']); + +/** + * set font family + * @name setFontFamily + * @methodOf Kinetic.TextPath.prototype + * @param {String} fontFamily + */ + +/** + * set font size + * @name setFontSize + * @methodOf Kinetic.TextPath.prototype + * @param {int} fontSize + */ + +/** + * set font style. Can be "normal", "italic", or "bold". "normal" is the default. + * @name setFontStyle + * @methodOf Kinetic.TextPath.prototype + * @param {String} fontStyle + */ + +/** + * set text fill color + * @name setTextFill + * @methodOf Kinetic.TextPath.prototype + * @param {String} textFill + */ + +/** + * set text stroke color + * @name setFontStroke + * @methodOf Kinetic.TextPath.prototype + * @param {String} textStroke + */ + +/** + * set text stroke width + * @name setTextStrokeWidth + * @methodOf Kinetic.TextPath.prototype + * @param {int} textStrokeWidth + */ + +/** + * set text + * @name setText + * @methodOf Kinetic.TextPath.prototype + * @param {String} text + */ + +/** + * get font family + * @name getFontFamily + * @methodOf Kinetic.TextPath.prototype + */ + +/** + * get font size + * @name getFontSize + * @methodOf Kinetic.TextPath.prototype + */ + +/** + * get font style + * @name getFontStyle + * @methodOf Kinetic.TextPath.prototype + */ + +/** + * get text fill color + * @name getTextFill + * @methodOf Kinetic.TextPath.prototype + */ + +/** + * get text stroke color + * @name getTextStroke + * @methodOf Kinetic.TextPath.prototype + */ + +/** + * get text stroke width + * @name getTextStrokeWidth + * @methodOf Kinetic.TextPath.prototype + */ + +/** + * get text + * @name getText + * @methodOf Kinetic.TextPath.prototype + */