>>=y,p-=y),p<15&&(c+=z[i++]<>>=y=v>>>24,p-=y,!(16&(y=v>>>16&255))){if(0==(64&y)){v=_[(65535&v)+(c&(1<>>=y,p-=y,(y=s-a)>3,c&=(1<<(p-=w<<3))-1,t.next_in=i,t.next_out=s,t.avail_in=i>>24&255)+(t>>>8&65280)+((65280&t)<<8)+((255&t)<<24)}function s(){this.mode=0,this.last=!1,this.wrap=0,this.havedict=!1,this.flags=0,this.dmax=0,this.check=0,this.total=0,this.head=null,this.wbits=0,this.wsize=0,this.whave=0,this.wnext=0,this.window=null,this.hold=0,this.bits=0,this.length=0,this.offset=0,this.extra=0,this.lencode=null,this.distcode=null,this.lenbits=0,this.distbits=0,this.ncode=0,this.nlen=0,this.ndist=0,this.have=0,this.next=null,this.lens=new I.Buf16(320),this.work=new I.Buf16(288),this.lendyn=null,this.distdyn=null,this.sane=0,this.back=0,this.was=0}function a(t){var e;return t&&t.state?(e=t.state,t.total_in=t.total_out=e.total=0,t.msg="",e.wrap&&(t.adler=1&e.wrap),e.mode=P,e.last=0,e.havedict=0,e.dmax=32768,e.head=null,e.hold=0,e.bits=0,e.lencode=e.lendyn=new I.Buf32(i),e.distcode=e.distdyn=new I.Buf32(n),e.sane=1,e.back=-1,N):U}function o(t){var e;return t&&t.state?((e=t.state).wsize=0,e.whave=0,e.wnext=0,a(t)):U}function h(t,e){var r,i;return t&&t.state?(i=t.state,e<0?(r=0,e=-e):(r=1+(e>>4),e<48&&(e&=15)),e&&(e<8||15=s.wsize?(I.arraySet(s.window,e,r-s.wsize,s.wsize,0),s.wnext=0,s.whave=s.wsize):(i<(n=s.wsize-s.wnext)&&(n=i),I.arraySet(s.window,e,r-i,n,s.wnext),(i-=n)?(I.arraySet(s.window,e,r-i,i,0),s.wnext=i,s.whave=s.wsize):(s.wnext+=n,s.wnext===s.wsize&&(s.wnext=0),s.whave>>8&255,r.check=B(r.check,E,2,0),l=u=0,r.mode=2;break}if(r.flags=0,r.head&&(r.head.done=!1),!(1&r.wrap)||(((255&u)<<8)+(u>>8))%31){t.msg="incorrect header check",r.mode=30;break}if(8!=(15&u)){t.msg="unknown compression method",r.mode=30;break}if(l-=4,k=8+(15&(u>>>=4)),0===r.wbits)r.wbits=k;else if(k>r.wbits){t.msg="invalid window size",r.mode=30;break}r.dmax=1<>8&1),512&r.flags&&(E[0]=255&u,E[1]=u>>>8&255,r.check=B(r.check,E,2,0)),l=u=0,r.mode=3;case 3:for(;l<32;){if(0===o)break t;o--,u+=i[s++]<>>8&255,E[2]=u>>>16&255,E[3]=u>>>24&255,r.check=B(r.check,E,4,0)),l=u=0,r.mode=4;case 4:for(;l<16;){if(0===o)break t;o--,u+=i[s++]<>8),512&r.flags&&(E[0]=255&u,E[1]=u>>>8&255,r.check=B(r.check,E,2,0)),l=u=0,r.mode=5;case 5:if(1024&r.flags){for(;l<16;){if(0===o)break t;o--,u+=i[s++]<>>8&255,r.check=B(r.check,E,2,0)),l=u=0}else r.head&&(r.head.extra=null);r.mode=6;case 6:if(1024&r.flags&&(o<(c=r.length)&&(c=o),c&&(r.head&&(k=r.head.extra_len-r.length,r.head.extra||(r.head.extra=new Array(r.head.extra_len)),I.arraySet(r.head.extra,i,s,c,k)),512&r.flags&&(r.check=B(r.check,i,c,s)),o-=c,s+=c,r.length-=c),r.length))break t;r.length=0,r.mode=7;case 7:if(2048&r.flags){if(0===o)break t;for(c=0;k=i[s+c++],r.head&&k&&r.length<65536&&(r.head.name+=String.fromCharCode(k)),k&&c>9&1,r.head.done=!0),t.adler=r.check=0,r.mode=12;break;case 10:for(;l<32;){if(0===o)break t;o--,u+=i[s++]<>>=7&l,l-=7&l,r.mode=27;break}for(;l<3;){if(0===o)break t;o--,u+=i[s++]<>>=1)){case 0:r.mode=14;break;case 1:if(j(r),r.mode=20,6!==e)break;u>>>=2,l-=2;break t;case 2:r.mode=17;break;case 3:t.msg="invalid block type",r.mode=30}u>>>=2,l-=2;break;case 14:for(u>>>=7&l,l-=7&l;l<32;){if(0===o)break t;o--,u+=i[s++]<>>16^65535)){t.msg="invalid stored block lengths",r.mode=30;break}if(r.length=65535&u,l=u=0,r.mode=15,6===e)break t;case 15:r.mode=16;case 16:if(c=r.length){if(o>>=5,l-=5,r.ndist=1+(31&u),u>>>=5,l-=5,r.ncode=4+(15&u),u>>>=4,l-=4,286>>=3,l-=3}for(;r.have<19;)r.lens[A[r.have++]]=0;if(r.lencode=r.lendyn,r.lenbits=7,S={bits:r.lenbits},x=T(0,r.lens,0,19,r.lencode,0,r.work,S),r.lenbits=S.bits,x){t.msg="invalid code lengths set",r.mode=30;break}r.have=0,r.mode=19;case 19:for(;r.have>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break t;o--,u+=i[s++]<>>=_,l-=_,r.lens[r.have++]=b;else{if(16===b){for(z=_+2;l>>=_,l-=_,0===r.have){t.msg="invalid bit length repeat",r.mode=30;break}k=r.lens[r.have-1],c=3+(3&u),u>>>=2,l-=2}else if(17===b){for(z=_+3;l>>=_)),u>>>=3,l-=3}else{for(z=_+7;l>>=_)),u>>>=7,l-=7}if(r.have+c>r.nlen+r.ndist){t.msg="invalid bit length repeat",r.mode=30;break}for(;c--;)r.lens[r.have++]=k}}if(30===r.mode)break;if(0===r.lens[256]){t.msg="invalid code -- missing end-of-block",r.mode=30;break}if(r.lenbits=9,S={bits:r.lenbits},x=T(D,r.lens,0,r.nlen,r.lencode,0,r.work,S),r.lenbits=S.bits,x){t.msg="invalid literal/lengths set",r.mode=30;break}if(r.distbits=6,r.distcode=r.distdyn,S={bits:r.distbits},x=T(F,r.lens,r.nlen,r.ndist,r.distcode,0,r.work,S),r.distbits=S.bits,x){t.msg="invalid distances set",r.mode=30;break}if(r.mode=20,6===e)break t;case 20:r.mode=21;case 21:if(6<=o&&258<=h){t.next_out=a,t.avail_out=h,t.next_in=s,t.avail_in=o,r.hold=u,r.bits=l,R(t,d),a=t.next_out,n=t.output,h=t.avail_out,s=t.next_in,i=t.input,o=t.avail_in,u=r.hold,l=r.bits,12===r.mode&&(r.back=-1);break}for(r.back=0;g=(C=r.lencode[u&(1<>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break t;o--,u+=i[s++]<>v)])>>>16&255,b=65535&C,!(v+(_=C>>>24)<=l);){if(0===o)break t;o--,u+=i[s++]<>>=v,l-=v,r.back+=v}if(u>>>=_,l-=_,r.back+=_,r.length=b,0===g){r.mode=26;break}if(32&g){r.back=-1,r.mode=12;break}if(64&g){t.msg="invalid literal/length code",r.mode=30;break}r.extra=15&g,r.mode=22;case 22:if(r.extra){for(z=r.extra;l>>=r.extra,l-=r.extra,r.back+=r.extra}r.was=r.length,r.mode=23;case 23:for(;g=(C=r.distcode[u&(1<>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break t;o--,u+=i[s++]<>v)])>>>16&255,b=65535&C,!(v+(_=C>>>24)<=l);){if(0===o)break t;o--,u+=i[s++]<>>=v,l-=v,r.back+=v}if(u>>>=_,l-=_,r.back+=_,64&g){t.msg="invalid distance code",r.mode=30;break}r.offset=b,r.extra=15&g,r.mode=24;case 24:if(r.extra){for(z=r.extra;l>>=r.extra,l-=r.extra,r.back+=r.extra}if(r.offset>r.dmax){t.msg="invalid distance too far back",r.mode=30;break}r.mode=25;case 25:if(0===h)break t;if(c=d-h,r.offset>c){if((c=r.offset-c)>r.whave&&r.sane){t.msg="invalid distance too far back",r.mode=30;break}p=c>r.wnext?(c-=r.wnext,r.wsize-c):r.wnext-c,c>r.length&&(c=r.length),m=r.window}else m=n,p=a-r.offset,c=r.length;for(hc?(m=R[T+a[v]],A[I+a[v]]):(m=96,0),h=1<>S)+(u-=h)]=p<<24|m<<16|_|0,0!==u;);for(h=1<>=1;if(0!==h?(E&=h-1,E+=h):E=0,v++,0==--O[b]){if(b===w)break;b=e[r+a[v]]}if(k>>7)]}function U(t,e){t.pending_buf[t.pending++]=255&e,t.pending_buf[t.pending++]=e>>>8&255}function P(t,e,r){t.bi_valid>c-r?(t.bi_buf|=e<>c-t.bi_valid,t.bi_valid+=r-c):(t.bi_buf|=e<>>=1,r<<=1,0<--e;);return r>>>1}function Z(t,e,r){var i,n,s=new Array(g+1),a=0;for(i=1;i<=g;i++)s[i]=a=a+r[i-1]<<1;for(n=0;n<=e;n++){var o=t[2*n+1];0!==o&&(t[2*n]=j(s[o]++,o))}}function W(t){var e;for(e=0;e>1;1<=r;r--)G(t,s,r);for(n=h;r=t.heap[1],t.heap[1]=t.heap[t.heap_len--],G(t,s,1),i=t.heap[1],t.heap[--t.heap_max]=r,t.heap[--t.heap_max]=i,s[2*n]=s[2*r]+s[2*i],t.depth[n]=(t.depth[r]>=t.depth[i]?t.depth[r]:t.depth[i])+1,s[2*r+1]=s[2*i+1]=n,t.heap[1]=n++,G(t,s,1),2<=t.heap_len;);t.heap[--t.heap_max]=t.heap[1],function(t,e){var r,i,n,s,a,o,h=e.dyn_tree,u=e.max_code,l=e.stat_desc.static_tree,f=e.stat_desc.has_stree,d=e.stat_desc.extra_bits,c=e.stat_desc.extra_base,p=e.stat_desc.max_length,m=0;for(s=0;s<=g;s++)t.bl_count[s]=0;for(h[2*t.heap[t.heap_max]+1]=0,r=t.heap_max+1;r<_;r++)p<(s=h[2*h[2*(i=t.heap[r])+1]+1]+1)&&(s=p,m++),h[2*i+1]=s,u>=7;i>>=1)if(1&r&&0!==t.dyn_ltree[2*e])return o;if(0!==t.dyn_ltree[18]||0!==t.dyn_ltree[20]||0!==t.dyn_ltree[26])return h;for(e=32;e>>3,(s=t.static_len+3+7>>>3)<=n&&(n=s)):n=s=r+5,r+4<=n&&-1!==e?J(t,e,r,i):4===t.strategy||s===n?(P(t,2+(i?1:0),3),K(t,z,C)):(P(t,4+(i?1:0),3),function(t,e,r,i){var n;for(P(t,e-257,5),P(t,r-1,5),P(t,i-4,4),n=0;n>>8&255,t.pending_buf[t.d_buf+2*t.last_lit+1]=255&e,t.pending_buf[t.l_buf+t.last_lit]=255&r,t.last_lit++,0===e?t.dyn_ltree[2*r]++:(t.matches++,e--,t.dyn_ltree[2*(A[r]+u+1)]++,t.dyn_dtree[2*N(e)]++),t.last_lit===t.lit_bufsize-1},r._tr_align=function(t){P(t,2,3),L(t,m,z),function(t){16===t.bi_valid?(U(t,t.bi_buf),t.bi_buf=0,t.bi_valid=0):8<=t.bi_valid&&(t.pending_buf[t.pending++]=255&t.bi_buf,t.bi_buf>>=8,t.bi_valid-=8)}(t)}},{"../utils/common":41}],53:[function(t,e,r){"use strict";e.exports=function(){this.input=null,this.next_in=0,this.avail_in=0,this.total_in=0,this.output=null,this.next_out=0,this.avail_out=0,this.total_out=0,this.msg="",this.state=null,this.data_type=2,this.adler=0}},{}],54:[function(t,e,r){"use strict";e.exports="function"==typeof setImmediate?setImmediate:function(){var t=[].slice.apply(arguments);t.splice(1,0,0),setTimeout.apply(null,t)}},{}]},{},[10])(10)});
--------------------------------------------------------------------------------
/script.js:
--------------------------------------------------------------------------------
1 | /* If you are looking here then I assume you have some interest in understanding how to
2 | * manipulate the WAD3 format. If you are creating a free application for the benefit
3 | * of the community then feel free to sift through and copy any code that you like.
4 | * If you have any questions, contact me at
5 | * joe((at))fortunatelyjoe.com <---- See how I fooled the bots there!
6 | *
7 | * Good luck in your programming endeavors!
8 | *
9 | * -Joe */
10 |
11 |
12 |
13 | "use strict";
14 |
15 | var textures = []; // A global array containing all the texture objects
16 | var file_name = "" // store the name of the currently open WAD file
17 |
18 |
19 | /********************************************************
20 | * upload
21 | *
22 | * Handles the file upload button
23 | ********************************************************/
24 | function upload() {
25 | // Clear any previous loaded textures
26 | textures = [];
27 |
28 | // Clear any previous loaded textures from document texture list
29 | var select = document.getElementById("texture_list");
30 | for(var i = select.options.length - 1 ; i >= 0 ; i--)
31 | select.remove(i);
32 |
33 | // Get the file
34 | var file = document.getElementById("upload").files[0];
35 | file_name = file.name
36 | // Create a FileReader to read file
37 | var reader = new FileReader();
38 |
39 | // Set up callback function so when the FilReader is finished, it passes the ArrayBuffer
40 | // to the parseWad function
41 | reader.onload = function() {
42 | parseWad(reader.result);
43 | }
44 |
45 | // Start converting file to ArrayBuffer
46 | reader.readAsArrayBuffer(file);
47 |
48 | }
49 |
50 |
51 |
52 | /********************************************************
53 | * onSelectTexture
54 | *
55 | * Event handler for when a texture is selected from the
56 | * texture list.
57 | ********************************************************/
58 | function onSelectTexture() {
59 | var index = document.getElementById("texture_list").selectedIndex;
60 | displayTexture(textures[index]);
61 |
62 | // Update rename box with current texture name
63 | document.getElementById("rename").value = textures[index].name;
64 | }
65 |
66 |
67 |
68 | /********************************************************
69 | * parseWad
70 | *
71 | * Takes an ArrayBuffer of a WAD file and parses it.
72 | ********************************************************/
73 | function parseWad(buffer) {
74 |
75 | // Create DataView to view the buffer by types
76 | var dv = new DataView(buffer);
77 |
78 | // Is this a valid WAD3 file?
79 | if (!isValidWad(dv)) {
80 | alert("File is not a valid WAD3 file.");
81 | return;
82 | }
83 |
84 | // Get the WAD header
85 | var header = getWadHeader(dv);
86 |
87 | // Get the WAD Directory Entries (List of texture file details)
88 | var entries = getWadEntries(dv, header.dirOffset, header.nEntries); // Global
89 |
90 | // Create the texture objects
91 | for (var i = 0; i < entries.length; i++) {
92 | textures[i] = retrieveTexture(dv, entries[i]);
93 | }
94 |
95 | // Populate texture list
96 | for (var i = 0; i < entries.length; i++) {
97 | var option = document.createElement("OPTION");
98 | option.text = entries[i].name;
99 | option.value = entries[i].name;
100 | document.getElementById("texture_list").options.add(option);
101 | }
102 |
103 | // Display first texture as default
104 | var texture = retrieveTexture(dv, entries[0]);
105 | document.getElementById("texture_list").selectedIndex = 0
106 | displayTexture(texture);
107 | }
108 |
109 |
110 |
111 | /********************************************************
112 | * getWadHeader
113 | *
114 | * Takes a DataView object of the WAD file and parses the
115 | * WAD header into an object.
116 | *
117 | *Header Specification (12 bytes)
118 | * 4 Magic Number "WAD3"
119 | * 4 nDir (The number of directory entries)
120 | * 4 nDirOffset Offset into file where entries start
121 | ********************************************************/
122 | function getWadHeader(dv) {
123 | var header = {};
124 |
125 | // Get the number of entries
126 | header.nEntries = dv.getUint32(4, true);
127 |
128 | // Get directory offset (directory contains all the file entries)
129 | header.dirOffset = dv.getUint32(8, true);
130 |
131 | return header;
132 | }
133 |
134 |
135 |
136 | /********************************************************
137 | * isWadValid
138 | *
139 | * Takes a DataView object of the WAD file and checks for
140 | * the magic string "WAD3". Returns true if valid, false
141 | * if not.
142 | ********************************************************/
143 | function isValidWad(dv) {
144 | // Read magic string and make sure this is a valid WAD3 File
145 | if (dv.getUint8(0) != 0x57 || dv.getUint8(1) != 0x41 || dv.getUint8(2) != 0x44 || dv.getUint8(3) != 0x33) // "WAD3"
146 | return false;
147 |
148 | return true;
149 | }
150 |
151 |
152 |
153 | /********************************************************
154 | * getWadEntries
155 | *
156 | * Takes a DataView object of the WAD file and parses the
157 | * collection of WAD entries into an array of objects.
158 | *
159 | * Directory Entry Specification (32 bytes)
160 | * 4 nFilePos Absolute offset to file's location
161 | * 4 nDiskSize Size of the file
162 | * 4 nSize Uncompressed size
163 | * 1 nType Type of entry
164 | * 1 bCompression 0 if not compressed
165 | * 2 nDUmmy Unused
166 | * 16 szName Name of file (null terminated)
167 | ********************************************************/
168 | function getWadEntries(dv, dirOffset, nEntries) {
169 | var entrySize = 32;
170 | var entries = [];
171 |
172 | for (var i = 0; i < nEntries; i++) {
173 |
174 | // Object to hold entry
175 | var currEntry = {};
176 |
177 | // Offset to start of current entry
178 | var entryPos = dirOffset + i * entrySize;
179 |
180 | // Offset property
181 | currEntry.offset = dv.getUint32(entryPos, true);
182 |
183 | // Size property
184 | currEntry.size = dv.getUint32(entryPos + 4, true);
185 |
186 | // Uncompressed Size property
187 | currEntry.uSize = dv.getUint32(entryPos + 8, true);
188 |
189 | // Type property
190 | currEntry.type = dv.getUint8(entryPos + 12);
191 |
192 | // Compressed State property
193 | currEntry.isCompressed = dv.getUint8(entryPos + 13);
194 |
195 | // Name String property
196 | currEntry.name = dataViewToString(dv, entryPos + 16, 16);
197 |
198 | // Add entry to entries array
199 | entries.push(currEntry);
200 |
201 | }
202 |
203 | return entries;
204 | }
205 |
206 |
207 |
208 | /********************************************************
209 | * dataViewToString
210 | *
211 | * Takes a DataView object of the WAD file, a starting
212 | * offset, and a maximum length and converts that portion
213 | * of the dataView to a string.
214 | ********************************************************/
215 | function dataViewToString(dv, start, len) {
216 | var str = "";
217 | for (var i = 0; i < len; i++) {
218 | // Get the ASCII code
219 | var charCode = dv.getUint8(start + i);
220 |
221 | // End loop if NULL-terminator
222 | if (charCode == 0) break;
223 |
224 | // Add character to name string
225 | str += String.fromCharCode(charCode);
226 | }
227 | return str;
228 | }
229 |
230 |
231 |
232 | /********************************************************
233 | * retrieveTexture
234 | *
235 | * Takes a DataView object of the WAD file, and the
236 | * the texture's directory entry object and creates
237 | * a texture object.
238 | *
239 | * Texture File Specification (file header is 40 bytes)
240 | * 16 szName Name of texture file (null terminated)
241 | * 4 nWidth Width of texture in pixels
242 | * 4 nHeight Height of texture in pixels
243 | * 4 offset0 relative Offset to level 0 MIP texture
244 | * 4 offset1 relative Offset to level 1 MIP texture
245 | * 4 offset2 relative Offset to level 2 MIP texture
246 | * 4 offset3 relative offset to level 3 MIP texture
247 | *---OFFSETS ARE RELATIVE TO BEGINNING OF FILE HEADER)---
248 | * VAR tex0 MIP texture level 0
249 | * VAR tex1 MIP texture level 1
250 | * VAR tex2 MIP texture level 2
251 | * VAR tex3 MIP texture level 3
252 | * 2 nColors Number of colors in palette (Max 256)
253 | * 768 palette Color table, 256 triplets of (R, G, B)
254 | * 2 padding
255 | ********************************************************/
256 | function retrieveTexture(dv, dirEntry) {
257 | var texture = {};
258 | var offset = dirEntry.offset; // Offset of actual texture within file
259 |
260 | // Name
261 | texture.name = dataViewToString(dv, offset, 16);
262 |
263 | // Width/Height
264 | texture.width = dv.getUint32(offset + 16, true);
265 | texture.height = dv.getUint32(offset + 20, true);
266 |
267 | // MIP Texture Offsets by level
268 | var mipOffset = [];
269 | mipOffset[0] = dv.getUint32(offset + 24, true);
270 | mipOffset[1] = dv.getUint32(offset + 28, true);
271 | mipOffset[2] = dv.getUint32(offset + 32, true);
272 | mipOffset[3] = dv.getUint32(offset + 36, true);
273 |
274 | // Read in MIP Textures by level
275 | texture.mipLevel = [];
276 | for (var level = 0; level < 4; level++) {
277 | texture.mipLevel[level] = [];
278 |
279 | // Read the pixels (Note, these are not RGB values, they are references to the palette)
280 | // The texture dimensions are divided by a power of 4 for each additional MIP level /4 /16 /64
281 | var nPixels = (texture.width * texture.height) / Math.pow(4, level);
282 | for (var i = 0; i < nPixels; i++) {
283 | texture.mipLevel[level][i] = dv.getUint8(offset + mipOffset[level] + i, true);
284 | }
285 | }
286 |
287 | // Read in palette
288 | texture.palette = [];
289 |
290 | // Palette is at the end of the file. We find the palette by starting at the file offset, fast-forward
291 | // to end of file, then back off by the size of the palette 768 (256 * 3)
292 | var paletteOffset = offset + dirEntry.size - 768 - 2;
293 | for (var i = 0; i < 768; i += 3) {
294 | var r = dv.getUint8(paletteOffset + i, true);
295 | var g = dv.getUint8(paletteOffset + i + 1, true);
296 | var b = dv.getUint8(paletteOffset + i + 2, true);
297 |
298 | // Add the RGB object to the palette array
299 | texture.palette.push({r:r, g:g, b:b});
300 | }
301 | return texture;
302 | }
303 |
304 |
305 |
306 | /********************************************************
307 | * displayTexture
308 | *
309 | * Takes a texture object and displays it on the
310 | * "viewport" canvas
311 | ********************************************************/
312 | function displayTexture(texture) {
313 |
314 | // Get canvas context
315 | var canvas = document.getElementById("viewport");
316 | var ctx = canvas.getContext("2d");
317 |
318 | // Resize canvas to image
319 | canvas.height = texture.height;
320 | canvas.width = texture.width;
321 |
322 | // Get access to the canvas pixel buffer
323 | var imgData = ctx.createImageData(texture.width, texture.height);
324 |
325 | // Clear screen
326 | ctx.clearRect(0, 0, canvas.width, canvas.height);
327 |
328 | // Draw pixels to pixel buffer
329 | var nPixels = texture.width * texture.height;
330 | var imgDataIndex = 0;
331 | for (var i = 0; i < nPixels; i++) {
332 | var palIndex = texture.mipLevel[0][i];
333 | imgData.data[imgDataIndex + 0] = texture.palette[palIndex].r; // Red
334 | imgData.data[imgDataIndex + 1] = texture.palette[palIndex].g; // Green
335 | imgData.data[imgDataIndex + 2] = texture.palette[palIndex].b; // Blue
336 | imgData.data[imgDataIndex + 3] = 255; // Alpha
337 | imgDataIndex += 4;
338 | }
339 |
340 | // Send pixel buffer back to canvas
341 | ctx.putImageData(imgData, 0, 0);
342 | }
343 |
344 |
345 |
346 | /********************************************************
347 | * buildWad()
348 | *
349 | * Builds a binary WAD file using textures stored in the
350 | * global "textures" array variable and then initiates
351 | * the download.
352 | ********************************************************/
353 | function buildWad() {
354 | // Keep track of file offsets for later when we build directory entries
355 | var fileOffsets = [];
356 |
357 | // Keep track of the individual file lengths for later when we build directory entries
358 | var fileSizes = [];
359 |
360 | // Header size
361 | var headerSize = 12;
362 |
363 | // Calculate the size of the directory entries section
364 | var entriesSize = textures.length * 32;
365 |
366 | // Calculate size of the file/data section
367 | var fileSectionSize = 0;
368 | for (var i = 0; i < textures.length; i++) {
369 | var nPixels = textures[i].width * textures[i].height;
370 | fileSectionSize += 40 + nPixels + nPixels/4 + nPixels/16 + nPixels/64 + 2 + 768 + 2;
371 | }
372 |
373 | // Create a buffer to hold our file
374 | var buffer = new ArrayBuffer(headerSize + entriesSize + fileSectionSize);
375 |
376 | // Create a dataview so we can populate the buffer with specific data types
377 | var dv = new DataView(buffer);
378 |
379 | // File position
380 | var pos = 0;
381 |
382 | // Build header
383 | pos = putByte(dv, pos, 0x57); // W
384 | pos = putByte(dv, pos, 0x41); // A
385 | pos = putByte(dv, pos, 0x44); // D
386 | pos = putByte(dv, pos, 0x33); // 3
387 | pos = put32(dv, pos, textures.length); // nDirs
388 | pos = put32(dv, pos, headerSize + fileSectionSize); // nDirOffset (entries start after file/data section)
389 |
390 |
391 | // Build File/Data section
392 | for (var i = 0; i < textures.length; i++) {
393 | fileOffsets.push(pos); // Note the current file position (used in directory entry later)
394 |
395 | pos = putStr16(dv, pos, textures[i].name); // Name string
396 | pos = put32(dv, pos, textures[i].width); // Width
397 | pos = put32(dv, pos, textures[i].height); // Height
398 |
399 | // Calculate MIP texture offsets
400 | var nPixels = textures[i].height * textures[i].width;
401 | var mipOffset0 = 40;
402 | var mipOffset1 = mipOffset0 + nPixels;
403 | var mipOffset2 = mipOffset1 + nPixels/4;
404 | var mipOffset3 = mipOffset2 + nPixels/16;
405 |
406 | // Write the MIP offsets
407 | pos = put32(dv, pos, mipOffset0); // MIP Level 0 offset
408 | pos = put32(dv, pos, mipOffset1); // MIP Level 1 offset
409 | pos = put32(dv, pos, mipOffset2); // MIP Level 2 offset
410 | pos = put32(dv, pos, mipOffset3); // MIP Level 3 offset
411 |
412 | // Write the MIP texture data by level
413 | for (var level = 0; level < 4; level++) {
414 |
415 | // Write all pixels within that layer
416 | var currLevel = textures[i].mipLevel[level];
417 | var currLength = currLevel.length;
418 | for (var pixel = 0; pixel < currLength; pixel++) {
419 |
420 | // Write pixel
421 | pos = putByte(dv, pos, currLevel[pixel]);
422 | }
423 | }
424 |
425 | // Write the palette
426 | pos = put16(dv, pos, 256); // Number of colors used
427 | var palette = textures[i].palette;
428 | for (var palIndex = 0; palIndex < 256; palIndex++) {
429 |
430 | // Write palette entry
431 | pos = putByte(dv, pos, palette[palIndex].r); // Red
432 | pos = putByte(dv, pos, palette[palIndex].g); // Green
433 | pos = putByte(dv, pos, palette[palIndex].b); // Blue
434 | }
435 |
436 | // 2 bytes of padding following palette
437 | pos = put16(dv, pos, 0);
438 |
439 | // Record the file size (current position - starting position)
440 | fileSizes[i] = pos - fileOffsets[i];
441 | }
442 |
443 | // Now build the directory entries
444 | for (var i = 0; i < textures.length; i++) {
445 | pos = put32(dv, pos, fileOffsets[i]); // offset of file in WAD
446 | pos = put32(dv, pos, fileSizes[i]); // file size
447 | pos = put32(dv, pos, fileSizes[i]); // uncompressed size (same, we don't support compression)
448 | pos = putByte(dv, pos, 67); // type (67 is what Wally uses, so it must be a good choice)
449 | pos = putByte(dv, pos, 0); // compression (0 because we don't support it)
450 | pos = put16(dv, pos, 0); // 2 dummy bytes
451 | pos = putStr16(dv, pos, textures[i].name); // texture name (16 bytes, null terminated)
452 | }
453 |
454 | saveData(buffer, "download.wad");
455 | }
456 |
457 |
458 |
459 | /********************************************************
460 | * binary put functions
461 | *
462 | * Takes a dataview object, a position (in bytes), and
463 | * a variable to write.
464 | ********************************************************/
465 | function putByte(dv, pos, data) {
466 | dv.setUint8(pos, data);
467 | return pos + 1;
468 | }
469 |
470 | function put16(dv, pos, data) {
471 | dv.setUint16(pos, data, true);
472 | return pos += 2;
473 | }
474 |
475 | function put32(dv, pos, data) {
476 | dv.setUint32(pos, data, true);
477 | return pos += 4;
478 | }
479 |
480 | function putStr16(dv, pos, str) {
481 | if (str.length > 15) {
482 | console.error("putStr16: Attempted to use string greater than length 15");
483 | return null;
484 | }
485 |
486 | var charLoop = str.length; // How many characters to add
487 | var nullLoop = 16 - str.length; // How many null terminators to add
488 |
489 | // Loop to add the string characters
490 | for (var i = 0; i < charLoop; i++) {
491 | var charCode = str.charCodeAt(i);
492 | dv.setUint8(pos + i, charCode);
493 | }
494 |
495 | // Loop to fill the any remaining bytes within the 16 length with null terminators
496 | for (var i = 0; i < nullLoop; i++) {
497 | dv.setUint8(pos + charLoop + i, 0);
498 | }
499 |
500 | return pos += 16;
501 | }
502 |
503 |
504 |
505 | /********************************************************
506 | * saveData
507 | *
508 | * Takes a data buffer (assumed to be a binary file) and
509 | * initiates the download.
510 | ********************************************************/
511 | function saveData(data, fileName) {
512 | var a = document.createElement("a");
513 | document.body.appendChild(a);
514 | a.style = "display: none";
515 |
516 | var blob = new Blob([data], {type: "octet/stream"});
517 | var url = window.URL.createObjectURL(blob);
518 | a.href = url;
519 | a.download = fileName;
520 | a.click();
521 | window.URL.revokeObjectURL(url);
522 | }
523 |
524 | /********************************************************
525 | * exportZip()
526 | *
527 | * Build a zip from all textures and save it
528 | ********************************************************/
529 | function exportZip() {
530 | var zip = new JSZip();
531 | // create a folder with the name of the file to prevent a mess on "extract here"
532 | var img = zip.folder(file_name.toLowerCase().replace(".wad", ""));
533 | var canvas = document.getElementById("viewport");
534 | for (var i = 0; i < textures.length; i++) {
535 | // display each texture to get it's data URL
536 | displayTexture(textures[i])
537 | // convert the dataURL to base64 and save it to a file
538 | img.file(`${textures[i].name}.png`, canvas.toDataURL().replace(/^data:image\/(png|jpg);base64,/, ""), {base64: true});
539 | }
540 | zip.generateAsync({type:"blob"})
541 | .then(function(content) {
542 | // see FileSaver.js
543 | saveAs(content, `${file_name}.zip`);
544 | });
545 | // re-display the currently selected texture
546 | displayTexture(textures[document.getElementById("texture_list").selectedIndex])
547 | }
548 |
549 | /********************************************************
550 | * add_img_handler()
551 | *
552 | * Handles the actions for adding an image to the WAD
553 | * collection.
554 | ********************************************************/
555 | function add_img_handler(){
556 | // Get the image file
557 | var imgFile = document.getElementById("add_img").files[0];
558 |
559 | // Create a FileReader and onload handler
560 | var reader = new FileReader();
561 | reader.onload = function(event){
562 |
563 | // Create image and onload handler
564 | var img = new Image();
565 | img.onload = function(){
566 |
567 | // Status Window
568 | var status = document.getElementById("status");
569 | status.value = "Status: Starting";
570 |
571 | // Make sure img dimensions are multiples of 16
572 | if (img.width % 16 || img.height % 16) {
573 | status.value = "Status: Error - Image dimensions must be multiples of 16."
574 | return;
575 | }
576 |
577 | // Disable interface
578 | setDisableInterface(true);
579 |
580 | // Set-up canvas
581 | var canvas = document.getElementById("viewport");
582 | var ctx = canvas.getContext("2d");
583 | canvas.width = img.width;
584 | canvas.height = img.height;
585 |
586 | // Draw image to canvas
587 | ctx.drawImage(img,0,0);
588 |
589 | // Get the pixel buffer from the canvas
590 | var buffer = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
591 |
592 | // Create web worker to handle the intense image processing routines
593 | var textureWorker = new Worker('create_texture.js');
594 |
595 | // Worker output variable
596 | var texture;
597 |
598 | // Message handler for textureWorker
599 | textureWorker.onmessage = function(event) {
600 |
601 | // Is incoming message a status update?
602 | if (event.data.type == "status") {
603 | status.value = "Status: " + event.data.msg;
604 | }
605 |
606 | // Otherwise its the processed texture
607 | else {
608 | texture = event.data.texture;
609 |
610 | // Display the image from palette form
611 | displayImg(texture.mipLevel[0], texture.palette);
612 |
613 | // Add the texture (handles document stuff)
614 | addTexture(texture);
615 |
616 | // Reenable interface
617 | setDisableInterface(false);
618 |
619 | console.log(texture.palette);
620 | }
621 | }
622 |
623 | // Start textureWorker thread
624 | textureWorker.postMessage({height:img.height, width:img.width, name:imgFile.name, buffer:buffer});
625 | }
626 | img.src = event.target.result;
627 | }
628 | // Invoke FileReader with the image file
629 | reader.readAsDataURL(imgFile);
630 | }
631 |
632 |
633 |
634 | /********************************************************
635 | * displayImg()
636 | *
637 | * Takes an array of pixels (which are not RGB values, but
638 | * references into the palette), and the palette itself
639 | * and displays the image in the viewport canvas.
640 | ********************************************************/
641 | function displayImg(refs, palette) {
642 | // Get image data to swap out
643 | var canvas = document.getElementById("viewport");
644 | var ctx = canvas.getContext("2d");
645 | var imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
646 | var buffer = imgData.data;
647 |
648 | // For all the palette references in the buffer
649 | var pixelIndex = 0;
650 | for (var i = 0; i < refs.length; i++) {
651 | // Grab color from palette
652 | var color = palette[refs[i]];
653 |
654 | // Build the pixel in the pixel buffer
655 | buffer[pixelIndex++] = color.r;
656 | buffer[pixelIndex++] = color.g;
657 | buffer[pixelIndex++] = color.b;
658 | buffer[pixelIndex++] = 255;
659 | }
660 |
661 | // Reinsert image data to canvas
662 | ctx.putImageData(imgData, 0, 0);
663 | }
664 |
665 |
666 |
667 | /********************************************************
668 | * addTexture()
669 | *
670 | * Takes a texture object handles integrating it into the
671 | * WAD's collection. Also handles updating document with
672 | * new texture's information.
673 | ********************************************************/
674 | function addTexture(texture) {
675 | // TODO ----------------- Generate mip textures
676 |
677 | // Add texture object
678 | textures.push(texture);
679 |
680 | // Add to list
681 | var list = document.getElementById("texture_list");
682 | var option = document.createElement("option");
683 | option.text = texture.name;
684 | list.add(option);
685 |
686 | }
687 |
688 |
689 |
690 | /********************************************************
691 | * removeTexture()
692 | *
693 | * removes the texture that is selected in the texture
694 | * list form, both from the form and from the internal
695 | * texture array.
696 | ********************************************************/
697 | function removeTexture() {
698 | // Make sure there are actually textures available to remove
699 | if (textures.length == 0)
700 | return;
701 |
702 | // Get list from document
703 | var list = document.getElementById("texture_list");
704 |
705 | // Get selected index from list
706 | var index = list.selectedIndex;
707 |
708 | // Make sure something is selected in list
709 | if (index == -1)
710 | return;
711 |
712 | // Remove from texture array without leaving gap
713 | textures.splice(index, 1);
714 |
715 | // Remove from list in document
716 | list.remove(index);
717 |
718 | // Clear the canvas of current image
719 | var canvas = document.getElementById("viewport");
720 | var ctx = canvas.getContext("2d");
721 | ctx.clearRect(0, 0, canvas.width, canvas.height);
722 |
723 | }
724 |
725 |
726 |
727 | /********************************************************
728 | * setDisableInterface()
729 | *
730 | * Enables/Disables the form elements depending on the
731 | * boolean argument.
732 | ********************************************************/
733 | function setDisableInterface(boolean) {
734 | document.getElementById("upload").disabled = boolean;
735 | document.getElementById("add_img").disabled = boolean;
736 | document.getElementById("download").disabled = boolean;
737 | document.getElementById("remove").disabled = boolean;
738 | document.getElementById("texture_list").disabled = boolean;
739 | document.getElementById("rename").disabled = boolean;
740 | }
741 |
742 |
743 |
744 | /********************************************************
745 | * rename()
746 | *
747 | * Renames a texture to whatever is in the rename form
748 | * element and updates the texture name in both the form
749 | * list and its internal object in the texture array.
750 | ********************************************************/
751 | function rename() {
752 | var newName = document.getElementById("rename").value;
753 | var status = document.getElementById("status");
754 | if (newName.length > 12) {
755 | status.value = "Status: Error - Name must be no longer than 12 characters";
756 | return;
757 | }
758 |
759 | // Make sure there are actually textures in the list
760 | if (textures.length == 0)
761 | return;
762 |
763 | // Get list from document
764 | var list = document.getElementById("texture_list");
765 |
766 | // Get selected index from list
767 | var index = list.selectedIndex;
768 |
769 | // Make sure something is selected in list
770 | if (index == -1)
771 | return;
772 |
773 | textures[index].name = newName;
774 |
775 | list.options[index].text = newName;
776 |
777 | status.value = "Status: Texture renamed.";
778 |
779 | }
780 |
781 |
--------------------------------------------------------------------------------
/styles.css:
--------------------------------------------------------------------------------
1 | body {
2 | text-align:center;
3 | background-color: #4d4d4d;
4 | font-family: Consolas,monaco,monospace;
5 | color: white;
6 | }
7 |
8 | button, input {
9 | font-family: Consolas,monaco,monospace;
10 | margin: 4px;
11 | background-color: #333333;
12 | border: none;
13 | color: #F5F5F5;
14 | padding: 15px 32px;
15 | text-align: center;
16 | text-decoration: none;
17 | display: inline-block;
18 | font-size: 16px;
19 | }
20 |
21 | input {
22 | width: 400px;
23 | }
24 |
25 | button {
26 | width: 150px;
27 | border: 1px solid white;
28 | }
29 |
30 | select {
31 | background-color: white;
32 | width: 200px;
33 | }
34 |
35 | canvas {
36 | background-color: black;
37 | margin: 10px;
38 | }
39 |
40 | table {
41 | width: 80%;
42 | margin: auto;
43 | }
44 |
45 | td {
46 | width: 30%;
47 | }
48 |
49 | #status {
50 | background-color: #333333;
51 | width: 100%;
52 | }
53 |
54 | #rename {
55 | background-color: white;
56 | color: #333333;
57 | }
58 |
59 | .center {
60 | text-align: center;
61 | }
62 |
63 |
--------------------------------------------------------------------------------