├── canvascycle ├── scenes.js ├── image.php ├── bitmap.js ├── framecount.js ├── cookie.js ├── oop.js ├── style.css ├── index.html ├── tools.js ├── tween.js ├── palette.js └── main.js ├── lbm2json ├── bitmap.h ├── geometry.h ├── bitmap.cpp ├── palette.h ├── sprite.h ├── main.cpp ├── geometry.cpp ├── palette.cpp └── sprite.cpp ├── LICENSE.md └── README.md /canvascycle/scenes.js: -------------------------------------------------------------------------------- 1 | var scenes = [ 2 | { 3 | name: 'TESTRAMP', 4 | title: 'Free Test Image', 5 | } 6 | ]; 7 | -------------------------------------------------------------------------------- /lbm2json/bitmap.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Library for handling 8-bit offscreen bitmaps. 3 | * Copyright 2001-2002 Joseph Huckaby. All rights reserved. 4 | **/ 5 | 6 | #define TRANS_LEVELS 32 7 | #define TRANS_BITS ((TRANS_LEVELS / 8) + 1) 8 | 9 | class Bitmap { 10 | public: 11 | short width, height; 12 | Rect *bounds; 13 | unsigned char *baseAddr; 14 | Palette *palette, *maskPalette; 15 | unsigned char transparency, add; 16 | short blackMask, pinkMask; 17 | char filename[256]; 18 | unsigned long modDate; 19 | short doMask; 20 | 21 | Bitmap(); 22 | Bitmap(short newWidth, short newHeight); 23 | ~Bitmap(); 24 | 25 | void init(); 26 | unsigned char getPixel(short x, short y); 27 | void setPixel(short x, short y, unsigned char color); 28 | }; 29 | 30 | extern unsigned char blitMode, phase; 31 | -------------------------------------------------------------------------------- /canvascycle/image.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lbm2json/geometry.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Library for handing simple geometric shapes. 3 | * Copyright 2001-2002 Joseph Huckaby. All rights reserved. 4 | **/ 5 | 6 | class Rect { 7 | public: 8 | short left, top, right, bottom; 9 | 10 | Rect(); 11 | Rect(short width, short height); 12 | Rect(short newLeft, short newTop, short newRight, short newBottom); 13 | Rect(Rect *source); 14 | Rect(Rect *source, short newLeft, short newTop); 15 | 16 | short width() {return (this->right - this->left);} 17 | short height() {return (this->bottom - this->top);} 18 | char empty() {return (this->left >= this->right || this->top >= this->bottom);} 19 | 20 | void constrain(Rect *bounds); 21 | void constrain(short leftPin, short topPin, short rightPin, short bottomPin); 22 | void offset(short horizDelta, short vertDelta); 23 | char ptIn(short xPoint, short yPoint); 24 | void zeroAlign(); 25 | void moveTo(short newLeft, short newTop); 26 | void inset(short xAmount, short yAmount); 27 | void set(short newLeft, short newTop, short newRight, short newBottom); 28 | void set(Rect *source); 29 | void setUnion(Rect *source); 30 | void fit(Rect *bounds); 31 | void intersect(Rect *sectRect); 32 | }; 33 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | **The MIT License (MIT)** 2 | 3 | *Copyright (c) 2010 - 2024 Joseph Huckaby and PixlCore.* 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /lbm2json/bitmap.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * Library for handling 8-bit offscreen bitmaps. 3 | * Copyright 2001-2002 Joseph Huckaby. All rights reserved. 4 | **/ 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | #include "geometry.h" 11 | #include "palette.h" 12 | #include "bitmap.h" 13 | 14 | unsigned char *transTables = NULL, phase = 0, blitMode = 2; 15 | unsigned long *ditherTables = NULL; 16 | 17 | void swap(short *a, short *b) { short temp; temp = *a; *a = *b; *b = temp; } 18 | 19 | Bitmap::Bitmap() { 20 | this->init(); 21 | } 22 | 23 | Bitmap::Bitmap (short newWidth, short newHeight) { 24 | this->init(); 25 | 26 | this->width = newWidth; 27 | this->height = newHeight; 28 | 29 | this->bounds = new Rect(this->width, this->height); 30 | 31 | this->baseAddr = (unsigned char *)malloc((long)this->width * (long)this->height); 32 | } 33 | 34 | Bitmap::~Bitmap() { 35 | if (this->baseAddr) free(this->baseAddr); 36 | if (this->palette) delete this->palette; 37 | if (this->maskPalette) delete this->maskPalette; 38 | if (this->bounds) delete this->bounds; 39 | } 40 | 41 | void Bitmap::init() { 42 | this->width = 0; 43 | this->height = 0; 44 | 45 | this->palette = new Palette(); 46 | this->maskPalette = new Palette(); 47 | 48 | this->bounds = NULL; 49 | this->baseAddr = NULL; 50 | 51 | this->transparency = 0; 52 | this->add = 0; 53 | this->blackMask = 0; 54 | this->pinkMask = 0; 55 | 56 | strcpy(this->filename, ""); 57 | this->modDate = 0; 58 | } 59 | 60 | unsigned char Bitmap::getPixel(short x, short y) { 61 | return *(this->baseAddr + (long)((long)this->width * (long)y) + (long)x); 62 | } 63 | 64 | void Bitmap::setPixel(short x, short y, unsigned char color) { 65 | *(this->baseAddr + (long)((long)this->width * (long)y) + (long)x) = color; 66 | } 67 | 68 | -------------------------------------------------------------------------------- /lbm2json/palette.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Library for handling 8-bit palettes. 3 | * Copyright 2001-2002 Joseph Huckaby. All rights reserved. 4 | **/ 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | #define MAX_COLORS 256 11 | #define MAX_CYCLES 256 12 | 13 | typedef struct { 14 | unsigned char blue, green, red, padding; /* color intensities 0..255 */ 15 | } color_register_32; /* size = 4 bytes */ 16 | 17 | typedef struct { 18 | short pad1; /* reserved for future use; store 0 here */ 19 | short rate; /* color cycle rate */ 20 | short reverse; 21 | unsigned char low, high; /* lower and upper color registers selected */ 22 | } c_range; 23 | 24 | class Palette { 25 | public: 26 | color_register_32 *colors, *baseColors; 27 | c_range *cycles; 28 | short numColors, numCycles; 29 | 30 | Palette(); 31 | ~Palette(); 32 | 33 | void setupCycles(); 34 | void cycle(color_register_32 *sourceColors, unsigned long timeNow, float speedAdjust, char blendShift); 35 | void fade(color_register_32 destColor, short frame, short max); 36 | void fade(Palette *destPalette, short frame, short max); 37 | void invert(); 38 | }; 39 | 40 | extern char USE_BLEND_SHIFT; 41 | extern short CYCLE_SPEED; 42 | extern char ENABLE_CYCLING; 43 | 44 | extern color_register_32 setRGB(unsigned char red, unsigned char green, unsigned char blue); 45 | extern char colorEqual(color_register_32 a, color_register_32 b); 46 | extern void copyColors(register color_register_32 *source_colors, register color_register_32 *dest_colors); 47 | extern color_register_32 fadeColor(color_register_32 sourceColor, color_register_32 destColor, short frame, short max); 48 | extern color_register_32 fadeColor(color_register_32 sourceColor, color_register_32 destColor, long frame, long max); 49 | extern unsigned char findColorIndex(color_register_32 *colors, color_register_32 color); 50 | extern void makeGradient(color_register_32 *colors, short low, short high, color_register_32 lowColor, color_register_32 highColor); 51 | extern color_register_32 RGBtoHSV( color_register_32 rgbColor ); 52 | extern color_register_32 HSVtoRGB( color_register_32 hsvColor ); 53 | 54 | -------------------------------------------------------------------------------- /lbm2json/sprite.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Library for loading DeluxePaint II LBM/BBM files. 3 | * Copyright 2001-2002 Joseph Huckaby. All rights reserved. 4 | **/ 5 | 6 | typedef struct { 7 | char id[4]; 8 | unsigned long length; 9 | } chunk; 10 | 11 | #define msk_none 0 12 | #define msk_has_mask 1 13 | #define msk_has_transparent_color 2 14 | #define msk_lasso 3 15 | 16 | /* Choice of compression algorithm applied to the rows of all 17 | * source and mask planes. "cmpByteRun1" is the byte run encoding 18 | * described in Appendix C. Do not compress across rows! */ 19 | #define cmp_none 0 20 | #define cmp_byte_run 1 21 | 22 | #define MAX_CYCLES 256 23 | 24 | // BMHD 25 | typedef struct { 26 | unsigned short width, height; /* raster width & height in pixels */ 27 | short x, y; /* pixel position for this image */ 28 | unsigned char num_planes; /* # source bitplanes */ 29 | unsigned char masking; 30 | unsigned char compression; 31 | unsigned char pad1; /* unused; for consistency, put 0 here */ 32 | unsigned short transparent_color; /* transparent "color number" (sort of) */ 33 | unsigned char x_aspect, y_aspect; /* pixel aspect, a ratio width : height */ 34 | short page_width, page_height; /* source "page" size in pixels */ 35 | } bitmap_header; 36 | 37 | // CMAP 38 | typedef struct { 39 | unsigned char red, green, blue; /* color intensities 0..255 */ 40 | } color_register; /* size = 3 bytes */ 41 | 42 | class Sprite { 43 | public: 44 | Bitmap *bitmap; 45 | short x, y; 46 | char visible, error, prepareBlits, doMask, blendShift, isAlpha, ditherAlpha, priority; 47 | unsigned long lastFrameCount, lastFrameTicks; 48 | float cycleSpeed; 49 | Rect *sourceRect; 50 | Sprite *nextSprite; 51 | Sprite *timeline; 52 | Sprite *alphaSprite; 53 | char stencil[256]; 54 | 55 | Sprite(); 56 | Sprite(char *newFilename); 57 | ~Sprite(); 58 | 59 | void init(); 60 | void cycle(unsigned long timeNow); 61 | void prepPaletteForAlpha(color_register_32 *colors); 62 | void load_bitmap(char *newFilename); 63 | void load_alpha(char *newFilename); 64 | Bitmap *load_from_lbm(char *newFilename); 65 | }; 66 | 67 | extern color_register_32 black; 68 | extern color_register_32 white; 69 | 70 | extern short check_id(char *id, char *check); 71 | extern void endian_swap(long *value); 72 | extern void endian_swap(unsigned long *value); 73 | extern void endian_swap(short *value); 74 | extern void endian_swap(unsigned short *value); -------------------------------------------------------------------------------- /canvascycle/bitmap.js: -------------------------------------------------------------------------------- 1 | // 8-bit Bitmap for use in HTML5 Canvas 2 | // Copyright (c) 2010 - 2024 Joseph Huckaby and PixlCore. 3 | // MIT Licensed: https://github.com/jhuckaby/canvascycle/blob/main/LICENSE.md 4 | 5 | Class.create( 'Bitmap', { 6 | 7 | width: 0, 8 | height: 0, 9 | pixels: null, 10 | palette: null, 11 | drawCount: 0, 12 | optPixels: null, 13 | 14 | __construct: function(img) { 15 | // class constructor 16 | this.width = img.width; 17 | this.height = img.height; 18 | this.palette = new Palette( img.colors, img.cycles ); 19 | this.pixels = img.pixels; 20 | }, 21 | 22 | optimize: function() { 23 | // prepare bitmap for optimized rendering (only refresh pixels that changed) 24 | var optColors = []; 25 | for (var idx = 0; idx < 256; idx++) optColors[idx] = 0; 26 | 27 | // mark animated colors in palette 28 | var cycles = this.palette.cycles; 29 | for (var idx = 0, len = cycles.length; idx < len; idx++) { 30 | var cycle = cycles[idx]; 31 | if (cycle.rate) { 32 | // cycle is animated 33 | for (idy = cycle.low; idy <= cycle.high; idy++) { 34 | optColors[idy] = 1; 35 | } 36 | } 37 | } 38 | 39 | // create array of pixel offsets which are animated 40 | var optPixels = this.optPixels = []; 41 | var pixels = this.pixels; 42 | var j = 0; 43 | var i = 0; 44 | var x, y; 45 | var xmax = this.width, ymax = this.height; 46 | 47 | for (y = 0; y < ymax; y++) { 48 | for (x = 0; x < xmax; x++) { 49 | if (optColors[pixels[j]]) optPixels[i++] = j; 50 | j++; 51 | } // x loop 52 | } // y loop 53 | }, 54 | 55 | render: function(imageData, optimize) { 56 | // render pixels into canvas imageData object 57 | var colors = this.palette.getRawTransformedColors(); 58 | var data = imageData.data; 59 | var pixels = this.pixels; 60 | 61 | if (optimize && this.drawCount && this.optPixels) { 62 | // only redraw pixels that are part of animated cycles 63 | var optPixels = this.optPixels; 64 | var i, j, clr; 65 | 66 | for (var idx = 0, len = optPixels.length; idx < len; idx++) { 67 | j = optPixels[idx]; 68 | clr = colors[ pixels[j] ]; 69 | i = j * 4; 70 | data[i + 0] = clr[0]; // red 71 | data[i + 1] = clr[1]; // green 72 | data[i + 2] = clr[2]; // blue 73 | data[i + 3] = 255; // alpha 74 | } 75 | } 76 | else { 77 | // draw every single pixel 78 | var i = 0; 79 | var j = 0; 80 | var x, y, clr; 81 | var xmax = this.width, ymax = this.height; 82 | 83 | for (y = 0; y < ymax; y++) { 84 | for (x = 0; x < xmax; x++) { 85 | clr = colors[ pixels[j] ]; 86 | data[i + 0] = clr[0]; // red 87 | data[i + 1] = clr[1]; // green 88 | data[i + 2] = clr[2]; // blue 89 | data[i + 3] = 255; // alpha 90 | i += 4; 91 | j++; 92 | } 93 | } 94 | } 95 | 96 | this.drawCount++; 97 | } 98 | 99 | } ); 100 | -------------------------------------------------------------------------------- /canvascycle/framecount.js: -------------------------------------------------------------------------------- 1 | // Basic Frame Counting Class 2 | // For displaying current and average frames per second 3 | // Author: Joseph Huckaby 4 | // Copyright (c) 2010 Joseph Huckaby 5 | // Usage: FrameCount.count() // for every frame 6 | 7 | var FrameCount = { 8 | 9 | current: 0, 10 | average: 0, 11 | frameCount: 0, 12 | lastSecond: 0, 13 | startTime: 0, 14 | totalFrames: 0, 15 | ie: !!navigator.userAgent.match(/MSIE/), 16 | visible: true, 17 | 18 | init: function() { 19 | // create floating widget 20 | if (this.visible) { 21 | var html = '
Waiting for frames...
'; 22 | 23 | if (this.ie) { 24 | setTimeout( function() { 25 | document.body.insertAdjacentHTML('beforeEnd', 26 | '
' + html + '
' 27 | ); 28 | }, 1000 ); 29 | } 30 | else { 31 | var div = document.createElement('DIV'); 32 | div.style.position = 'fixed'; 33 | div.style.zIndex = '9999'; 34 | div.style.left = '0px'; 35 | div.style.top = '0px'; 36 | div.style.width = '100%'; 37 | div.innerHTML = html; 38 | document.getElementsByTagName('body')[0].appendChild(div); 39 | } 40 | } 41 | }, 42 | 43 | update: function() { 44 | // update display 45 | var div = document.getElementById('d_framecount'); 46 | if (div) { 47 | var html = ''; 48 | 49 | html += ''; 50 | html += ''; 51 | html += ''; 52 | html += ''; 53 | html += '
Current FPS:' + this.current + '
Average FPS:' + this.average + '
Total Frames:' + this.totalFrames + '
'; 54 | 55 | html += '
Reset'; 56 | 57 | div.innerHTML = html; 58 | } 59 | }, 60 | 61 | reset: function() { 62 | this.current = 0; 63 | this.average = 0; 64 | this.frameCount = 0; 65 | this.lastSecond = 0; 66 | this.startTime = 0; 67 | this.totalFrames = 0; 68 | this.update(); 69 | }, 70 | 71 | _now_epoch: function() { 72 | // return current date/time in hi-res epoch seconds 73 | var _mydate = new Date(); 74 | return _mydate.getTime() / 1000; 75 | }, 76 | 77 | count: function() { 78 | // advance one frame 79 | var _now = this._now_epoch(); 80 | var _int_now = parseInt(_now, 10); 81 | if (_int_now != this.lastSecond) { 82 | this.totalFrames += this.frameCount; 83 | if (!this.startTime) this.startTime = _int_now; 84 | if (_int_now > this.startTime) this.average = this.totalFrames / (_int_now - this.startTime); 85 | else this.average = this.frameCount; 86 | 87 | this.current = this.frameCount; 88 | this.frameCount = 0; 89 | this.lastSecond = _int_now; 90 | 91 | if (this.visible) this.update(); 92 | } 93 | this.frameCount++; 94 | } 95 | 96 | }; 97 | 98 | -------------------------------------------------------------------------------- /canvascycle/cookie.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * cookie.js 3 | * A simple cookie library supporting hash trees 4 | * Requires Joe Tools for merge_objects() and serialize(). 5 | * 6 | * var tree = new CookieTree(); 7 | * tree.set( "foo", "bar" ); 8 | * tree.set( "complex", { hello: "there", array: [1,2,3] } ); 9 | * tree.save(); 10 | * 11 | * Copyright (c) 2007 - 2024 Joseph Huckaby and PixlCore. 12 | * MIT Licensed: https://github.com/jhuckaby/canvascycle/blob/main/LICENSE.md 13 | */ 14 | 15 | /* if (!window.merge_objects || !window.serialize) 16 | alert("ERROR: cookie.js requires tools.js."); */ 17 | 18 | function CookieTree(args) { 19 | // class constructor 20 | if (args) { 21 | for (var key in args) this[key] = args[key]; 22 | } 23 | 24 | if (!this.expires) { 25 | var now = new Date(); 26 | now.setFullYear( now.getFullYear() + 10 ); // 10 years from now 27 | this.expires = now.toGMTString(); 28 | } 29 | 30 | this.parse(); 31 | }; 32 | 33 | CookieTree.prototype.domain = location.hostname; 34 | CookieTree.prototype.path = location.pathname; 35 | 36 | CookieTree.prototype.parse = function() { 37 | // parse document.cookie into hash tree 38 | this.tree = {}; 39 | var cookies = document.cookie.split(/\;\s*/); 40 | for (var idx = 0, len = cookies.length; idx < len; idx++) { 41 | var cookie_raw = cookies[idx]; 42 | if (cookie_raw.match(/^CookieTree=(.+)$/)) { 43 | var cookie = null; 44 | var cookie_raw = unescape( RegExp.$1 ); 45 | // Debug.trace("Cookie", "Parsing cookie: " + cookie_raw); 46 | try { 47 | eval( "cookie = " + cookie_raw + ";" ); 48 | } 49 | catch (e) { 50 | // Debug.trace("Cookie", "Failed to parse cookie."); 51 | cookie = {}; 52 | } 53 | 54 | this.tree = merge_objects( this.tree, cookie ); 55 | idx = len; 56 | } 57 | } 58 | }; 59 | 60 | CookieTree.prototype.get = function(key) { 61 | // get tree branch given value (top level) 62 | return this.tree[key]; 63 | }; 64 | 65 | CookieTree.prototype.set = function(key, value) { 66 | // set tree branch to given value (top level) 67 | this.tree[key] = value; 68 | }; 69 | 70 | CookieTree.prototype.save = function() { 71 | // serialize tree and save back into document.cookie 72 | var cookie_raw = 'CookieTree=' + escape(serialize(this.tree)); 73 | 74 | if (!this.path.match(/\/$/)) { 75 | this.path = this.path.replace(/\/[^\/]+$/, "") + '/'; 76 | } 77 | 78 | cookie_raw += '; expires=' + this.expires; 79 | cookie_raw += '; domain=' + this.domain; 80 | cookie_raw += '; path=' + this.path; 81 | 82 | // Debug.trace("Cookie", "Saving cookie: " + cookie_raw); 83 | 84 | document.cookie = cookie_raw; 85 | }; 86 | 87 | CookieTree.prototype.remove = function() { 88 | // remove cookie from document 89 | var cookie_raw = 'CookieTree={}'; 90 | 91 | if (!this.path.match(/\/$/)) { 92 | this.path = this.path.replace(/\/[^\/]+$/, "") + '/'; 93 | } 94 | 95 | var now = new Date(); 96 | now.setFullYear( now.getFullYear() - 1 ); // last year 97 | cookie_raw += '; expires=' + now.toGMTString(); 98 | 99 | cookie_raw += '; domain=' + this.domain; 100 | cookie_raw += '; path=' + this.path; 101 | 102 | document.cookie = cookie_raw; 103 | }; 104 | -------------------------------------------------------------------------------- /lbm2json/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "geometry.h" 4 | #include "palette.h" 5 | #include "bitmap.h" 6 | #include "sprite.h" 7 | 8 | // for filename in `ls -1 *.LBM`; do echo $filename; /Users/jhuckaby/Projects/lbm2json/build/Debug/lbm2json $filename; done; 9 | 10 | 11 | int main (int argc, char * const argv[]) { 12 | Sprite *sprite = NULL; 13 | Bitmap *bitmap = NULL; 14 | Palette *palette = NULL; 15 | color_register_32 color; 16 | c_range cycle; 17 | unsigned char pixel; 18 | 19 | /* unsigned char uc; 20 | printf("Size of unsigned char: %d\n", sizeof(uc)); 21 | 22 | unsigned char us; 23 | printf("Size of unsigned short: %d\n", sizeof(us)); 24 | 25 | unsigned long ul; 26 | printf("Size of unsigned long: %d\n", sizeof(ul)); 27 | */ 28 | 29 | // std::cout << "Hello, World!\n"; 30 | 31 | printf("Executable: %s\n", argv[0]); 32 | printf("Argument: %s\n", argv[1]); 33 | 34 | char path[80]; 35 | strcpy( path, argv[1] ); 36 | 37 | sprite = new Sprite( path ); 38 | bitmap = sprite->bitmap; 39 | palette = bitmap->palette; 40 | 41 | char filename[80]; 42 | char* strLastSlash = strrchr( path, '/' ); 43 | if (!strLastSlash) strcpy(filename, path); 44 | else strcpy(filename, strLastSlash+1); 45 | 46 | // write raw binary file first (totally raw pixels, no header) 47 | char out_file[256]; 48 | sprintf(out_file, "%s.bin", path); 49 | printf("Output Binary File: %s\n", out_file); 50 | 51 | FILE* out; 52 | out = (FILE*)fopen(out_file, "wb"); 53 | if (!out) { 54 | printf("Could not fopen file for writing: %s\n", out_file); 55 | return 0; 56 | } 57 | fwrite(bitmap->baseAddr, 1, (unsigned long)bitmap->width * (unsigned long)bitmap->height, out); 58 | fclose(out); 59 | 60 | // now write JSON file 61 | sprintf(out_file, "%s.json", path); 62 | printf("Output JSON File: %s\n", out_file); 63 | 64 | out = (FILE*)fopen(out_file, "w"); 65 | if (!out) { 66 | printf("Could not fopen file for writing: %s\n", out_file); 67 | return 0; 68 | } 69 | 70 | fprintf(out, "{"); 71 | 72 | // basic info 73 | fprintf(out, "filename:'%s',width:%d,height:%d", filename, bitmap->width, bitmap->height); 74 | 75 | // palette 76 | fprintf(out, ",colors:["); 77 | for (int idx = 0; idx < palette->numColors; idx++) { 78 | color = palette->baseColors[idx]; 79 | if (idx) fprintf(out, ","); 80 | fprintf(out, "[%d,%d,%d]", color.red, color.green, color.blue); 81 | } 82 | fprintf(out, "]"); 83 | 84 | // cycles 85 | fprintf(out, ",cycles:["); 86 | for (int idx = 0; idx < palette->numCycles; idx++) { 87 | cycle = palette->cycles[idx]; 88 | if (idx) fprintf(out, ","); 89 | fprintf(out, "{reverse:%d,rate:%d,low:%d,high:%d}", cycle.reverse, cycle.rate, cycle.low, cycle.high); 90 | } 91 | fprintf(out, "]"); 92 | 93 | // pixels 94 | fprintf(out, ",pixels:["); 95 | int count = 0; 96 | for (int y = 0; y < bitmap->height; y++) { 97 | 98 | // printf("\nROW %d\n", y); 99 | fprintf(out, "\n"); 100 | 101 | for (int x = 0; x < bitmap->width; x++) { 102 | pixel = bitmap->getPixel( x, y ); 103 | if (count) fprintf(out, ","); 104 | fprintf(out, "%d", pixel); 105 | count++; 106 | } // x loop 107 | } // y loop 108 | fprintf(out, "]"); 109 | 110 | fprintf(out, "}\n"); 111 | fclose(out); 112 | 113 | delete sprite; 114 | 115 | printf("Done! Exiting\n"); 116 | 117 | return 0; 118 | } 119 | -------------------------------------------------------------------------------- /lbm2json/geometry.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * Library for handing simple geometric shapes. 3 | * Copyright 2001-2002 Joseph Huckaby. All rights reserved. 4 | **/ 5 | 6 | #include "geometry.h" 7 | 8 | Rect::Rect() { 9 | this->left = 0; 10 | this->top = 0; 11 | this->right = 0; 12 | this->bottom = 0; 13 | } 14 | 15 | Rect::Rect(short width, short height) { 16 | this->left = 0; 17 | this->top = 0; 18 | this->right = width; 19 | this->bottom = height; 20 | } 21 | 22 | Rect::Rect(short newLeft, short newTop, short newRight, short newBottom) { 23 | this->left = newLeft; 24 | this->top = newTop; 25 | this->right = newRight; 26 | this->bottom = newBottom; 27 | } 28 | 29 | Rect::Rect(Rect *source) { 30 | this->set(source); 31 | } 32 | 33 | Rect::Rect(Rect *source, short newLeft, short newTop) { 34 | this->left = source->left; 35 | this->top = source->top; 36 | this->right = source->right; 37 | this->bottom = source->bottom; 38 | this->moveTo(newLeft, newTop); 39 | } 40 | 41 | void Rect::constrain(Rect *bounds) { 42 | if (this->left < bounds->left) this->left = bounds->left; 43 | if (this->top < bounds->top) this->top = bounds->top; 44 | if (this->right > bounds->right) this->right = bounds->right; 45 | if (this->bottom > bounds->bottom) this->bottom = bounds->bottom; 46 | 47 | if (this->left > bounds->right) this->left = bounds->right; 48 | if (this->top > bounds->bottom) this->top = bounds->bottom; 49 | if (this->right < bounds->left) this->right = bounds->left; 50 | if (this->bottom < bounds->top) this->bottom = bounds->top; 51 | } 52 | 53 | void Rect::constrain(short leftPin, short topPin, short rightPin, short bottomPin) { 54 | if (this->left < leftPin) this->left = leftPin; 55 | if (this->top < topPin) this->top = topPin; 56 | if (this->right > rightPin) this->right = rightPin; 57 | if (this->bottom > bottomPin) this->bottom = bottomPin; 58 | 59 | if (this->left > rightPin) this->left = rightPin; 60 | if (this->top > bottomPin) this->top = bottomPin; 61 | if (this->right < leftPin) this->right = leftPin; 62 | if (this->bottom < topPin) this->bottom = topPin; 63 | } 64 | 65 | void Rect::offset(short horizDelta, short vertDelta) { 66 | this->left += horizDelta; 67 | this->top += vertDelta; 68 | this->right += horizDelta; 69 | this->bottom += vertDelta; 70 | } 71 | 72 | char Rect::ptIn(short xPoint, short yPoint) { 73 | if (xPoint>=this->left && xPointright && yPoint>=this->top && yPointbottom) return 1; 74 | return 0; 75 | } 76 | 77 | void Rect::zeroAlign() { 78 | this->right -= this->left; 79 | this->bottom -= this->top; 80 | this->left -= this->left; 81 | this->top -= this->top; 82 | } 83 | 84 | void Rect::moveTo(short newLeft, short newTop) { 85 | this->offset(newLeft - this->left, newTop - this->top); 86 | } 87 | 88 | void Rect::inset(short xAmount, short yAmount) { 89 | this->left += xAmount; 90 | this->top += yAmount; 91 | this->right -= xAmount; 92 | this->bottom -= yAmount; 93 | } 94 | 95 | void Rect::set(short newLeft, short newTop, short newRight, short newBottom) { 96 | this->left = newLeft; 97 | this->top = newTop; 98 | this->right = newRight; 99 | this->bottom = newBottom; 100 | } 101 | 102 | void Rect::set(Rect *source) { 103 | this->left = source->left; 104 | this->top = source->top; 105 | this->right = source->right; 106 | this->bottom = source->bottom; 107 | } 108 | 109 | void Rect::setUnion(Rect *source) { 110 | if (source->left < this->left) this->left = source->left; 111 | if (source->top < this->top) this->top = source->top; 112 | if (source->right > this->right) this->right = source->right; 113 | if (source->bottom > this->bottom) this->bottom = source->bottom; 114 | } 115 | 116 | void Rect::fit(Rect *bounds) { 117 | if (this->width() > bounds->width()) this->right = this->left + bounds->width(); 118 | if (this->height() > bounds->height()) this->bottom = this->top + bounds->height(); 119 | 120 | if (this->left < bounds->left) this->offset(bounds->left - this->left, 0); 121 | if (this->top < bounds->top) this->offset(0, bounds->top - this->top); 122 | 123 | if (this->right > bounds->right) this->offset(bounds->right - this->right, 0); 124 | if (this->bottom > bounds->bottom) this->offset(bounds->bottom - this->bottom, 0); 125 | } 126 | 127 | void Rect::intersect(Rect *sectRect) { 128 | if (sectRect->left > this->left) this->left = sectRect->left; 129 | if (sectRect->top > this->top) this->top = sectRect->top; 130 | if (sectRect->right < this->right) this->right = sectRect->right; 131 | if (sectRect->bottom < this->bottom) this->bottom = sectRect->bottom; 132 | } 133 | 134 | -------------------------------------------------------------------------------- /canvascycle/oop.js: -------------------------------------------------------------------------------- 1 | /** 2 | * JavaScript Object Oriented Programming Framework 3 | * Author: Joseph Huckaby 4 | * Copyright (c) 2008 - 2024 Joseph Huckaby and PixlCore. 5 | * MIT Licensed: https://github.com/jhuckaby/canvascycle/blob/main/LICENSE.md 6 | **/ 7 | 8 | function _var_exists(name) { 9 | // return true if var exists in "global" context, false otherwise 10 | try { 11 | eval('var foo = ' + name + ';'); 12 | } 13 | catch (e) { 14 | return false; 15 | } 16 | return true; 17 | } 18 | 19 | var Namespace = { 20 | // simple namespace support for classes 21 | create: function(path) { 22 | // create namespace for class 23 | var container = null; 24 | while (path.match(/^(\w+)\.?/)) { 25 | var key = RegExp.$1; 26 | path = path.replace(/^(\w+)\.?/, ""); 27 | 28 | if (!container) { 29 | if (!_var_exists(key)) eval('window.' + key + ' = {};'); 30 | eval('container = ' + key + ';'); 31 | } 32 | else { 33 | if (!container[key]) container[key] = {}; 34 | container = container[key]; 35 | } 36 | } 37 | }, 38 | prep: function(name) { 39 | // prep namespace for new class 40 | if (name.match(/^(.+)\.(\w+)$/)) { 41 | var path = RegExp.$1; 42 | name = RegExp.$2; 43 | Namespace.create(path); 44 | } 45 | return { name: name }; 46 | } 47 | }; 48 | 49 | var Class = { 50 | // simple class factory 51 | create: function(name, members) { 52 | // generate new class with optional namespace 53 | 54 | // support Prototype-style calling convention 55 | if (!name && !members) { 56 | return( function() { 57 | if (this.initialize) this.initialize.apply(this, arguments); 58 | else if (this.__construct) this.__construct.apply(this, arguments); 59 | } ); 60 | } 61 | 62 | assert(name, "Must pass name to Class.create"); 63 | if (!members) members = {}; 64 | members.__parent = null; 65 | 66 | var ns = Namespace.prep(name); 67 | // var container = ns.container; 68 | var full_name = name; 69 | name = ns.name; 70 | 71 | members.__name = name; 72 | 73 | if (!members.__construct) members.__construct = function() {}; 74 | 75 | // container[name] = members.__construct; 76 | var obj = null; 77 | eval( full_name + ' = obj = members.__construct;' ); 78 | 79 | var static_members = members.__static; 80 | if (static_members) { 81 | for (var key in static_members) { 82 | obj[key] = static_members[key]; 83 | } 84 | } 85 | 86 | obj.prototype = members; 87 | obj.extend = obj.subclass = function(name, members) { 88 | Class.subclass( this, name, members ); 89 | }; 90 | obj.set = obj.add = function(members) { 91 | Class.add( this, members ); 92 | }; 93 | }, 94 | subclass: function(parent, name, members) { 95 | // subclass an existing class 96 | assert(parent, "Must pass parent class to Class.subclass"); 97 | assert(name, "Must pass name to Class.subclass"); 98 | if (!members) members = {}; 99 | members.__name = name; 100 | members.__parent = parent.prototype; 101 | 102 | var ns = Namespace.prep(name); 103 | // var container = ns.container; 104 | var subname = ns.name; 105 | 106 | var obj = null; 107 | 108 | if (members.__construct) { 109 | // explicit subclass constructor 110 | // container[subname] = members.__construct; 111 | eval( name + ' = obj = members.__construct;' ); 112 | } 113 | else { 114 | // inherit parent's constructor 115 | var code = parent.toString(); 116 | var args = code.substring( code.indexOf("(")+1, code.indexOf(")") ); 117 | var inner_code = code.substring( code.indexOf("{")+1, code.lastIndexOf("}") ); 118 | eval('members.__construct = ' + name + ' = obj = function ('+args+') {'+inner_code+'};'); 119 | } 120 | 121 | // inherit static from parent, if applicable 122 | if (parent.prototype.__static) { 123 | for (var key in parent.prototype.__static) { 124 | obj[key] = parent.prototype.__static[key]; 125 | } 126 | } 127 | 128 | var static_members = members.__static; 129 | if (static_members) { 130 | for (var key in static_members) { 131 | obj[key] = static_members[key]; 132 | } 133 | } 134 | 135 | obj.prototype = new parent(); 136 | // for (var key in parent.prototype) container[subname].prototype[key] = parent.prototype[key]; 137 | for (var key in members) obj.prototype[key] = members[key]; 138 | 139 | obj.extend = obj.subclass = function(name, members) { 140 | Class.subclass( this, name, members ); 141 | }; 142 | obj.set = obj.add = function(members) { 143 | Class.add( this, members ); 144 | }; 145 | }, 146 | add: function(obj, members) { 147 | // add members to an existing class 148 | for (var key in members) obj.prototype[key] = members[key]; 149 | }, 150 | require: function() { 151 | // make sure classes are loaded 152 | for (var idx = 0, len = arguments.length; idx < len; idx++) { 153 | assert( !!eval('window.' + arguments[idx]) ); 154 | } 155 | return true; 156 | } 157 | }; 158 | Class.extend = Class.subclass; 159 | Class.set = Class.add; 160 | 161 | if (!window.assert) window.assert = function(fact, msg) { 162 | // very simple assert 163 | if (!fact) return alert("ASSERT FAILED! " + msg); 164 | return fact; 165 | }; 166 | -------------------------------------------------------------------------------- /canvascycle/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #000; 3 | color: #888; 4 | font-family:Helvetica,sans-serif; font-size:11px; 5 | overflow: hidden; 6 | margin: 0; 7 | padding: 0; 8 | width: 2048px; 9 | } 10 | 11 | a { 12 | color: #888; 13 | text-decoration: none; 14 | } 15 | 16 | a:hover { 17 | text-decoration: underline; 18 | } 19 | 20 | #palette_display { 21 | /* margin: 15px 0px 15px 0px; */ 22 | width: 150px; 23 | } 24 | 25 | div.palette_color { 26 | float: left; 27 | width: 8px; 28 | height: 8px; 29 | border: 1px solid #555; 30 | margin: 1px 0px 0px 1px; 31 | } 32 | 33 | div.clear { 34 | clear: both; 35 | } 36 | 37 | #container { 38 | position: relative; 39 | display: none; 40 | left: 0px; 41 | top: 0px; 42 | } 43 | 44 | #d_header { 45 | margin-bottom: 10px; 46 | width: 640px; 47 | height: 30px; 48 | } 49 | 50 | #d_scene_selector { 51 | float: left; 52 | line-height: 30px; 53 | } 54 | 55 | #fe_scene { 56 | outline: 0; 57 | border: 0; 58 | background-color: #000; 59 | color: #888; 60 | font-size: 14px; 61 | padding: 3px; 62 | } 63 | 64 | #d_scene_btns { 65 | float: left; 66 | margin-left: 20px; 67 | margin-top: 3px; 68 | } 69 | 70 | #d_options_control { 71 | float: right; 72 | margin-top: 3px; 73 | } 74 | 75 | #btn_options_toggle { 76 | position: relative; 77 | left: 0px; 78 | } 79 | 80 | #d_loading { 81 | position: absolute; 82 | width: 32px; 83 | height: 32px; 84 | left: 0px; 85 | top: 0px; 86 | z-index: 3; 87 | background: url(images/loading.gif) no-repeat center center; 88 | } 89 | 90 | #mycanvas { 91 | float: left; 92 | z-index: 2; 93 | 94 | -webkit-transform: translate3d(0px, 0px, 0px) scale(1); 95 | -webkit-transform-origin: 0px 0px; 96 | 97 | -moz-transform: translate(0px 0px) scale(1); 98 | -moz-transform-origin: 0px 0px; 99 | 100 | -o-transform:translate(0px 0px) scale(1); 101 | -o-transform-origin:0px 0px; 102 | } 103 | 104 | #d_options { 105 | z-index: 1; 106 | position: relative; 107 | float: left; 108 | width: 150px; 109 | height: 480px; 110 | /* border:1px solid #444; */ 111 | margin-left: 15px; 112 | } 113 | 114 | #d_debug { 115 | margin-top: 7px; 116 | color: #555; 117 | font-weight: bold; 118 | } 119 | 120 | .section { 121 | margin-bottom: 20px; 122 | } 123 | 124 | div.label { 125 | font-weight: bold; 126 | color: #444; 127 | text-shadow: #777 -1px -1px 1px; 128 | margin: 1px 0px 3px 0px; 129 | } 130 | 131 | .button { 132 | display: inline-block; 133 | width: auto; 134 | height: 14px; 135 | 136 | padding: 2px 10px 2px 10px; 137 | /* margin-right: 1px; */ 138 | border-left: 1px solid #888; 139 | border-top: 1px solid #888; 140 | border-right: 1px solid #444; 141 | border-bottom: 1px solid #444; 142 | cursor:pointer; 143 | 144 | font-size:13px; color:#888; font-weight:bold; 145 | text-align: center; 146 | 147 | background: #444; 148 | background: -webkit-gradient( 149 | linear, 150 | left bottom, 151 | left top, 152 | color-stop(0, #444), 153 | color-stop(1, #000) 154 | ); 155 | background: -moz-linear-gradient( 156 | center bottom, 157 | #444 0%, 158 | #000 100% 159 | ); 160 | 161 | -webkit-user-select: none; 162 | } 163 | 164 | .button.left { 165 | border-top-left-radius: 8px; 166 | border-bottom-left-radius: 8px; 167 | -moz-border-radius-topleft: 8px; 168 | -moz-border-radius-bottomleft: 8px; 169 | -webkit-border-top-left-radius: 8px; 170 | -webkit-border-bottom-left-radius: 8px; 171 | } 172 | 173 | .button.right { 174 | border-top-right-radius: 8px; 175 | border-bottom-right-radius: 8px; 176 | -moz-border-radius-topright: 8px; 177 | -moz-border-radius-bottomright: 8px; 178 | -webkit-border-top-right-radius: 8px; 179 | -webkit-border-bottom-right-radius: 8px; 180 | } 181 | 182 | .button:hover, .button.hover { 183 | color: #aaa; 184 | 185 | background: #000; 186 | background: -webkit-gradient( 187 | linear, 188 | left bottom, 189 | left top, 190 | color-stop(0, rgb(0,0,0)), 191 | color-stop(1, rgb(0,0,0)) 192 | ); 193 | background: -moz-linear-gradient( 194 | center bottom, 195 | rgb(0,0,0) 0%, 196 | rgb(0,0,0) 100% 197 | ); 198 | 199 | /* box-shadow: rgba(255,255,255,0.8) 0px 0px 3px; 200 | -webkit-box-shadow: rgba(255,255,255,0.8) 0px 0px 3px; 201 | -moz-box-shadow: rgba(255,255,255,0.8) 0px 0px 3px; */ 202 | } 203 | 204 | .button.selected { 205 | border-left: 1px solid #444; 206 | border-top: 1px solid #444; 207 | border-right: 1px solid #666; 208 | border-bottom: 1px solid #666; 209 | 210 | background: #444; 211 | background: -webkit-gradient( 212 | linear, 213 | left bottom, 214 | left top, 215 | color-stop(0, #000), 216 | color-stop(1, #666) 217 | ); 218 | background: -moz-linear-gradient( 219 | center bottom, 220 | #000 0%, 221 | #666 100% 222 | ); 223 | 224 | color: #fff; 225 | 226 | cursor: default; 227 | } 228 | 229 | /* .button:active, .button.active { 230 | padding: 3px 10px 3px 10px; 231 | background-color:#ddd; 232 | border-left: 1px solid #777; 233 | border-top: 1px solid #777; 234 | border-right: 1px solid #ccc; 235 | border-bottom: 1px solid #ccc; 236 | } */ 237 | 238 | .button.thin { 239 | padding: 2px 6px 2px 6px; 240 | } 241 | 242 | #d_footer { 243 | position: fixed; 244 | left: 0px; 245 | top: 100%; 246 | margin-top: -20px; 247 | height: 20px; 248 | width: 100%; 249 | text-align: center; 250 | font-weight: bold; 251 | } 252 | 253 | #d_footer > span { 254 | padding: 0px 10px 0px 10px; 255 | } -------------------------------------------------------------------------------- /canvascycle/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Canvas Cycle: True 8-bit Color Cycling with HTML5 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 |
28 |
29 |
30 |
< Prev
Next >
31 |
32 |
33 |
Show Options »
34 |
35 |
36 |
37 |
38 | 39 | 40 | 41 |
42 |
SOUND:
43 |
44 |
Off
On
45 |
46 | 47 |
ZOOM:
48 |
49 |
Actual
Max
50 |
51 | 52 |
CYCLE SPEED:
53 |
54 |
¼
½
1
2
4
55 |
56 | 57 |
CYCLE MODE:
58 |
59 |
Standard
Blend
60 |
61 | 62 |
PALETTE:
63 |
64 | 65 |
66 |
67 | 68 |
69 | 70 |
71 | 72 | 77 | 78 | 99 | 100 | 132 | 133 | 134 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Old School Color Cycling with HTML5 2 | 3 | Anyone remember [Color cycling](https://en.wikipedia.org/wiki/Color_cycling) from the 90s? This was a technology often used in 8-bit video games of the era, to achieve interesting visual effects by cycling (shifting) the color palette. Back then video cards could only render 256 colors at a time, so a palette of selected colors was used. But the programmer could change this palette at will, and all the onscreen colors would instantly change to match. It was fast, and took virtually no memory. Thus began the era of color cycling. 4 | 5 | Most games used the technique to animate water, fire or other environmental effects. Unfortunately, more often than not this looked terrible, because the artist simply drew the scene once, picked some colors to be animated and set them to cycle. While this technically qualified as "color cycling", it looked more like a bad acid trip. For an example, just look at the water in [this game](https://www.youtube.com/watch?v=wfkEr3Bxoqg). 6 | 7 | However, there was one graphic artist who took the technique to a whole new level, and produced absolutely breathtaking color cycling scenes. [Mark J. Ferrari](https://www.markferrari.com/), who also illustrated all the original backgrounds for [LucasArts](https://en.wikipedia.org/wiki/Lucasfilm_Games) [Loom](https://en.wikipedia.org/wiki/Loom_%28video_game%29), and some for [The Secret of Monkey Island](https://en.wikipedia.org/wiki/The_Secret_of_Monkey_Island), invented his own unique ways of using color cycling for environmental effects that you really have to see to believe. These include rain, snow, ocean waves, moving fog, clouds, smoke, waterfalls, streams, lakes, and more. And all these effects are achieved without any layers or alpha channels -- just one single flat image with one 256 color palette. 8 | 9 | Unfortunately the art of color cycling died out in the late 90s, giving way to newer technologies like 3D rendering and full 32-bit "true color" games. However, 2D pixel graphics of old are making a comeback in recent years, with mobile devices and web games. I thought now would be the time to reintroduce color cycling, using open web technologies like the [HTML5](https://en.wikipedia.org/wiki/HTML5) [Canvas element](https://en.wikipedia.org/wiki/Canvas_element). 10 | 11 | This demo is an implementation of a full 8-bit color cycling engine, rendered into an HTML5 Canvas in real-time. I am using 35 of Mark's original 640x480 pixel masterpieces which you can explore, and I added some ambient environmental soundtracks to match. Please enjoy, and the source code is free for you to use in your own projects (download links at the bottom of the article). 12 | 13 | ![Screenshot](https://pixlcore.com/software/canvascycle/screenshot.png) 14 | 15 | - **[Launch Demo With Sound](http://www.effectgames.com/demos/canvascycle/)** 16 | - **[Launch Demo Without Sound](http://www.effectgames.com/demos/canvascycle/?sound=0)** 17 | 18 | ## Q & A with Mark J. Ferrari 19 | 20 | Hey everyone! Mark has generously donated some of his time to answer the most popular questions on his color cycling artwork. Please read about it here: [Q & A with Mark J. Ferrari](http://www.effectgames.com/effect/article.psp.html/joe/Q_A_with_Mark_J_Ferrari). 21 | 22 | ## BlendShift Cycling 23 | 24 | Those of you familiar with color cycling may notice something a little "different" about the palette animation in this engine. Many years ago I had an idea to improve color cycling by "fading" colors into each other as they shifted, to produce many "in between" frames, while preserving the overall "speed" of the cycling effect. This creates a much smoother appearance, and gives the illusion of more colors in the scene. I call this technique **BlendShift Cycling**. Someone may have invented this before me, but I've certainly never seen it used. 25 | 26 | You can really see the effect this has if you slow down the cycling speed (Click "Show Options" and click on either ¼ or ½), then turn BlendShift off and on by clicking on the "Standard" and "Blend" cycling modes. See the difference? The colors "shift" positions in whole steps when using Standard mode, but fade smoothly into each other when using BlendShift. If only I'd invented this trick 20 years ago when it really mattered! 27 | 28 | ## Optimization 29 | 30 | In order to achieve fast frame rates in the browser, I had to get a little crazy in the engine implementation. Rendering a 640x480 indexed image on a 32-bit RGB canvas means walking through and drawing 307,200 pixels per frame, in JavaScript. That's a very big array to traverse, and some browsers just couldn't keep up. To overcome this, I pre-process the images when they are first loaded, and grab the pixels that reference colors which are animated (i.e. are part of cycling sets in the palette). Those pixel X/Y offsets are stored in a separate, smaller array, and thus only the pixels that change are refreshed onscreen. 31 | 32 | The framerate is capped at 60 FPS. 33 | 34 | ## Amiga IFF / LBM Files 35 | 36 | Mark's scenes are actually [Amiga IFF / ILBM](https://en.wikipedia.org/wiki/ILBM) files, originally created with [Deluxe Paint](https://en.wikipedia.org/wiki/Deluxe_Paint) in DOS. Ah, those were the days! So, to make this work, I had to write a converter program which parses the files and extracts the pixels, the palette colors, and all the cycling information, and writes it out as something JavaScript can understand. The data is stored as [JSON](https://en.wikipedia.org/wiki/JSON) on disk, and delivered to the browser with gzip compression. The data sent over the wire ends up being about 100K per scene, which isn't too bad (most of the soundtracks are larger than that, haha). My converter script is written in C++, but included in the source package if you are interested. 37 | 38 | **Update:** I also wrote a Node.js implementation of my LBM converter utility: [lbmtool](https://github.com/jhuckaby/lbmtool) 39 | 40 | ## Browser Support 41 | 42 | The color cycling engine works in all modern browsers. 43 | 44 | ## Download Source 45 | 46 | Here is the JavaScript and C++ source code to my color cycling engine. I am releasing it under the [MIT License](https://github.com/jhuckaby/canvascycle/blob/main/LICENSE.md). The package comes with one test LBM image, converted to JSON. The actual artwork shown in the demo is copyright, and cannot be used. 47 | 48 | https://github.com/jhuckaby/canvascycle/archive/refs/heads/main.zip 49 | -------------------------------------------------------------------------------- /canvascycle/tools.js: -------------------------------------------------------------------------------- 1 | // Misc Tools 2 | // Copyright (c) 2010 - 2024 Joseph Huckaby and PixlCore. 3 | // MIT Licensed: https://github.com/jhuckaby/canvascycle/blob/main/LICENSE.md 4 | // getInnerWindowSize() was grabbed from: http://www.howtocreate.co.uk/tutorials/javascript/browserwindow 5 | 6 | function $(thingy) { 7 | // universal DOM lookup function, extends object with hide/show/addClass/removeClass 8 | // can pass in ID or actual DOM object reference 9 | var obj = (typeof(thingy) == 'string') ? document.getElementById(thingy) : thingy; 10 | if (obj && !obj.setOpacity) { 11 | obj.hide = function() { this.style.display = 'none'; return this; }; 12 | obj.show = function() { this.style.display = ''; return this; }; 13 | obj.addClass = function(name) { this.removeClass(name); this.className += ' ' + name; return this; }; 14 | 15 | obj.removeClass = function(name) { 16 | var classes = this.className.split(/\s+/); 17 | var idx = find_idx_in_array( classes, name ); 18 | if (idx > -1) { 19 | classes.splice( idx, 1 ); 20 | this.className = classes.join(' '); 21 | } 22 | return this; 23 | }; 24 | 25 | obj.setClass = function(name, enabled) { 26 | if (enabled) this.addClass(name); 27 | else this.removeClass(name); 28 | }; 29 | } 30 | return obj; 31 | } 32 | 33 | function GetTickCount() { 34 | // milliseconds since page load 35 | return Math.floor( (new Date()).getTime() - CanvasCycle.globalTimeStart ); 36 | } 37 | 38 | function getInnerWindowSize(dom) { 39 | // get size of inner window 40 | // From: http://www.howtocreate.co.uk/tutorials/javascript/browserwindow 41 | if (!dom) dom = window; 42 | var myWidth = 0, myHeight = 0; 43 | 44 | if( typeof( dom.innerWidth ) == 'number' ) { 45 | // Non-IE 46 | myWidth = dom.innerWidth; 47 | myHeight = dom.innerHeight; 48 | } 49 | else if( dom.document.documentElement && ( dom.document.documentElement.clientWidth || dom.document.documentElement.clientHeight ) ) { 50 | // IE 6+ in 'standards compliant mode' 51 | myWidth = dom.document.documentElement.clientWidth; 52 | myHeight = dom.document.documentElement.clientHeight; 53 | } 54 | else if( dom.document.body && ( dom.document.body.clientWidth || dom.document.body.clientHeight ) ) { 55 | // IE 4 compatible 56 | myWidth = dom.document.body.clientWidth; 57 | myHeight = dom.document.body.clientHeight; 58 | } 59 | return { width: myWidth, height: myHeight }; 60 | } 61 | 62 | function find_idx_in_array(arr, elem) { 63 | // return idx of elem in arr, or -1 if not found 64 | for (var idx = 0, len = arr.length; idx < len; idx++) { 65 | if (arr[idx] == elem) return idx; 66 | } 67 | return -1; 68 | } 69 | 70 | function isa_hash(arg) { 71 | // determine if arg is a hash 72 | return( !!arg && (typeof(arg) == 'object') && (typeof(arg.length) == 'undefined') ); 73 | } 74 | 75 | function isa_array(arg) { 76 | // determine if arg is an array or is array-like 77 | if (typeof(arg) == 'array') return true; 78 | return( !!arg && (typeof(arg) == 'object') && (typeof(arg.length) != 'undefined') ); 79 | } 80 | 81 | function merge_objects(a, b) { 82 | // merge keys from a and b into c and return c 83 | // b has precedence over a 84 | if (!a) a = {}; 85 | if (!b) b = {}; 86 | var c = {}; 87 | 88 | // also handle serialized objects for a and b 89 | if (typeof(a) != 'object') eval( "a = " + a ); 90 | if (typeof(b) != 'object') eval( "b = " + b ); 91 | 92 | for (var key in a) c[key] = a[key]; 93 | for (var key in b) c[key] = b[key]; 94 | 95 | return c; 96 | } 97 | 98 | function serialize(thingy, glue) { 99 | // serialize anything into json 100 | // or perl object notation (just set glue to '=>') 101 | if (!glue) glue = ':'; // default to json 102 | var stream = ''; 103 | 104 | if (typeof(thingy) == 'boolean') { 105 | stream += (thingy ? 'true' : 'false'); 106 | } 107 | else if (typeof(thingy) == 'number') { 108 | stream += thingy; 109 | } 110 | else if (typeof(thingy) == 'string') { 111 | stream += '"' + thingy.replace(/([\"\\])/g, '\\$1').replace(/\r/g, "\\r").replace(/\n/g, "\\n") + '"'; 112 | } 113 | else if (isa_hash(thingy)) { 114 | var num = 0; 115 | var buffer = []; 116 | for (var key in thingy) { 117 | buffer[num] = (key.match(/^[A-Za-z]\w*$/) ? key : ('"'+key+'"')) + glue + serialize(thingy[key], glue); 118 | num++; 119 | } 120 | stream += '{' + buffer.join(',') + '}'; 121 | } 122 | else if (isa_array(thingy)) { 123 | var buffer = []; 124 | for (var idx = 0, len = thingy.length; idx < len; idx++) { 125 | buffer[idx] = serialize(thingy[idx], glue); 126 | } 127 | stream += '[' + buffer.join(',') + ']'; 128 | } 129 | else { 130 | // unknown type, just return 0 131 | stream += '0'; 132 | } 133 | 134 | return stream; 135 | } 136 | 137 | (function() { 138 | // Browser detection 139 | var u = navigator.userAgent; 140 | var webkit = !!u.match(/webkit/i); 141 | var chrome = !!u.match(/Chrome/); 142 | var safari = !!u.match(/Safari/) && !chrome; 143 | var ie = !!u.match(/MSIE/); 144 | var ie6 = ie && !!u.match(/MSIE\s+6/); 145 | var ie7 = ie && !!u.match(/MSIE\s+7/); 146 | var ie8 = ie && !!u.match(/MSIE\s+8/); 147 | var moz = !safari && !ie; 148 | var op = !!window.opera; 149 | var mac = !!u.match(/Mac/i); 150 | var ff = !!u.match(/(Firefox|Minefield)/); 151 | var iphone = !!u.match(/iPhone/); 152 | var ipad = !!u.match(/iPad/); 153 | var snow = !!u.match(/Mac\s+OS\s+X\s+10\D[6789]/); 154 | var titanium = safari && !!u.match(/Titanium/); 155 | var android = !!u.match(/android/i); 156 | 157 | var ver = 0; 158 | if (ff && u.match(/Firefox\D+(\d+(\.\d+)?)/)) { 159 | ver = parseFloat( RegExp.$1 ); 160 | } 161 | else if (safari && u.match(/Version\D(\d+(\.\d+)?)/)) { 162 | ver = parseFloat( RegExp.$1 ); 163 | } 164 | else if (chrome && u.match(/Chrome\D(\d+(\.\d+)?)/)) { 165 | ver = parseFloat( RegExp.$1 ); 166 | } 167 | else if (ie && u.match(/MSIE\D+(\d+(\.\d+)?)/)) { 168 | ver = parseFloat( RegExp.$1 ); 169 | } 170 | else if (op && u.match(/Opera\D+(\d+(\.\d+)?)/)) { 171 | ver = parseFloat( RegExp.$1 ); 172 | } 173 | 174 | window.ua = { 175 | webkit: webkit, 176 | safari: safari, 177 | ie: ie, 178 | ie8: ie8, 179 | ie7: ie7, 180 | ie6: ie6, 181 | moz: moz, 182 | op: op, 183 | mac: mac, 184 | ff: ff, 185 | chrome: chrome, 186 | iphone: iphone, 187 | ipad: ipad, 188 | snow: snow, 189 | titanium: titanium, 190 | android: android, 191 | mobile: iphone || ipad || android, 192 | ver: ver 193 | }; 194 | })(); 195 | 196 | -------------------------------------------------------------------------------- /canvascycle/tween.js: -------------------------------------------------------------------------------- 1 | //// 2 | // Tween.js 3 | // Provides numerical obj property animation with easing. 4 | // Copyright (c) 2005 - 2008 Joseph Huckaby 5 | //// 6 | 7 | var TweenManager = { 8 | 9 | _tweens: {}, 10 | _nextId: 1, 11 | 12 | add: function(_args) { 13 | // add new tween to table 14 | var _tween = new Tween(_args); 15 | this._tweens[ this._nextId ] = _tween; 16 | _tween.id = this._nextId; 17 | this._nextId++; 18 | return _tween; 19 | }, 20 | 21 | logic: function(clock) { 22 | // update tweens 23 | for (var _id in this._tweens) { 24 | var _tween = this._tweens[_id]; 25 | _tween.logic(clock); 26 | if (_tween.destroyed) delete this._tweens[_id]; 27 | } 28 | }, 29 | 30 | removeAll: function() { 31 | // remove all tweens 32 | if (arguments.length) { 33 | var criteria = arguments[0]; 34 | var num_crit = 0; 35 | for (var key in criteria) num_crit++; 36 | 37 | for (var id in this._tweens) { 38 | var tween = this._tweens[id]; 39 | var matched = 0; 40 | for (var key in criteria) { 41 | if (tween[key] == criteria[key]) matched++; 42 | } 43 | if (matched == num_crit) { 44 | tween.destroyed = 1; 45 | delete this._tweens[id]; 46 | } 47 | } 48 | } 49 | else { 50 | for (var id in this._tweens) { 51 | var tween = this._tweens[id]; 52 | tween.destroyed = 1; 53 | } 54 | this._tweens = {}; 55 | } 56 | } 57 | 58 | }; 59 | TweenManager.tween = TweenManager.add; 60 | 61 | // Tween Object 62 | 63 | function Tween(_args) { 64 | // create new tween 65 | // args should contain: 66 | // target: target object 67 | // duration: length of animation in logic frames 68 | // mode: EaseIn, EaseOut, EaseInOut (omit or empty string for linear) 69 | // algorithm: Quadtaric, etc. 70 | // properties: { x: {start:0, end:150}, y: {start:0, end:250, filter:Math.floor} } 71 | if (!_args.algorithm && _args.algo) { _args.algorithm = _args.algo; delete _args.algo; } 72 | if (!_args.properties && _args.props) { _args.properties = _args.props; delete _args.props; } 73 | 74 | for (var _key in _args) this[_key] = _args[_key]; 75 | 76 | // linear shortcut 77 | if (!this.mode) this.mode = 'EaseIn'; 78 | if (!this.algorithm) this.algorithm = 'Linear'; 79 | 80 | this.require('target', 'duration', 'properties'); 81 | // if (typeof(this.target) != 'object') return alert("Tween: Target is not an object"); 82 | if (typeof(this.duration) != 'number') return alert("Tween: Duration is not a number"); 83 | if (typeof(this.properties) != 'object') return alert("Tween: Properties is not an object"); 84 | 85 | // setup properties 86 | for (var _key in this.properties) { 87 | var _prop = this.properties[_key]; 88 | if (typeof(_prop) == 'number') _prop = this.properties[_key] = { end: _prop }; 89 | if (typeof(_prop) != 'object') return alert("Tween: Property " + _key + " is not the correct format"); 90 | if (typeof(_prop.start) == 'undefined') _prop.start = this.target[_key]; 91 | 92 | if (_prop.start.toString().match(/^([\d\.]+)([a-zA-Z]+)$/) && !_prop.suffix) { 93 | _prop.start = RegExp.$1; 94 | _prop.suffix = RegExp.$3; 95 | _prop.end = _prop.end.toString().replace(/[^\d\.]+$/, ''); 96 | } 97 | if ((typeof(_prop.start) != 'number') && _prop.start.toString().match(/^\d+\.\d+$/)) { 98 | _prop.start = parseFloat( _prop.start ); 99 | } 100 | else if ((typeof(_prop.start) != 'number') && _prop.start.toString().match(/^\d+$/)) { 101 | _prop.start = parseInt( _prop.start, 10 ); 102 | } 103 | 104 | if ((typeof(_prop.end) != 'number') && _prop.end.toString().match(/^\d+\.\d+$/)) { 105 | _prop.end = parseFloat( _prop.end ); 106 | } 107 | else if ((typeof(_prop.end) != 'number') && _prop.end.toString().match(/^\d+$/)) { 108 | _prop.end = parseInt( _prop.end, 10 ); 109 | } 110 | 111 | if (typeof(_prop.start) != 'number') return alert("Tween: Property " + _key + ": start is not a number"); 112 | if (typeof(_prop.end) != 'number') return alert("Tween: Property " + _key + ": end is not a number"); 113 | if (_prop.filter && (typeof(_prop.filter) != 'function')) return alert("Tween: Property " + _key + ": filter is not a function"); 114 | } 115 | } 116 | 117 | Tween.prototype.destroyed = false; 118 | Tween.prototype.delay = 0; 119 | 120 | Tween.prototype.require = function() { 121 | // make sure required class members exist 122 | for (var _idx = 0, _len = arguments.length; _idx < _len; _idx++) { 123 | if (typeof(this[arguments[_idx]]) == 'undefined') { 124 | return alert("Tween: Missing required parameter: " + arguments[_idx]); 125 | } 126 | } 127 | return true; 128 | }; 129 | 130 | Tween.prototype.logic = function(clock) { 131 | // abort if our target is destroyed 132 | // (and don't call onTweenComplete) 133 | if (this.target.destroyed) { 134 | this.destroyed = true; 135 | return; 136 | } 137 | if (this.delay > 0) { 138 | this.delay--; 139 | if (this.delay <= 0) this.start = clock; 140 | else return; 141 | } 142 | if (!this.start) this.start = clock; 143 | 144 | // calculate current progress 145 | this.amount = (clock - this.start) / this.duration; 146 | if (this.amount >= 1.0) { 147 | this.amount = 1.0; 148 | this.destroyed = true; 149 | } 150 | 151 | // animate obj properties 152 | for (var _key in this.properties) { 153 | var _prop = this.properties[_key]; 154 | var _value = _prop.start + (ease(this.amount, this.mode, this.algorithm) * (_prop.end - _prop.start)); 155 | if (_prop.filter) _value = _prop.filter( _value ); 156 | 157 | /* console.log( "tweening: " + _key + ": " + serialize({ 158 | id: this.id, 159 | amount: this.amount, 160 | start: _prop.start, 161 | end: _prop.end, 162 | mode: this.mode, 163 | algorithm: this.algorithm, 164 | value: _value 165 | }) ); */ 166 | 167 | this.target[_key] = _prop.suffix ? ('' + _value + _prop.suffix) : _value; 168 | } 169 | 170 | // notify object that things are happening to it 171 | if (this.onTweenUpdate) this.onTweenUpdate(this); 172 | if (this.target.onTweenUpdate) this.target.onTweenUpdate(this); 173 | 174 | if (this.destroyed) { 175 | if (this.onTweenComplete) this.onTweenComplete(this); 176 | if (this.target.onTweenComplete) this.target.onTweenComplete(this); 177 | } 178 | }; 179 | 180 | // Static Utility Function for tweening a single property to a single point in an animation 181 | 182 | function tweenFrame(_start, _end, _amount, _mode, _algo) { 183 | return _start + (ease(_amount, _mode, _algo) * (_end - _start)); 184 | } 185 | 186 | // 187 | // Easing functions 188 | // 189 | 190 | var EaseAlgos = { 191 | Linear: function(_amount) { return _amount; }, 192 | Quadratic: function(_amount) { return Math.pow(_amount, 2); }, 193 | Cubic: function(_amount) { return Math.pow(_amount, 3); }, 194 | Quartetic: function(_amount) { return Math.pow(_amount, 4); }, 195 | Quintic: function(_amount) { return Math.pow(_amount, 5); }, 196 | Sine: function(_amount) { return 1 - Math.sin((1 - _amount) * Math.PI / 2); }, 197 | Circular: function(_amount) { return 1 - Math.sin(Math.acos(_amount)); } 198 | }; 199 | var EaseModes = { 200 | EaseIn: function(_amount, _algo) { return EaseAlgos[_algo](_amount); }, 201 | EaseOut: function(_amount, _algo) { return 1 - EaseAlgos[_algo](1 - _amount); }, 202 | EaseInOut: function(_amount, _algo) { 203 | return (_amount <= 0.5) ? EaseAlgos[_algo](2 * _amount) / 2 : (2 - EaseAlgos[_algo](2 * (1 - _amount))) / 2; 204 | } 205 | }; 206 | function ease(_amount, _mode, _algo) { 207 | return EaseModes[_mode]( _amount, _algo ); 208 | } 209 | -------------------------------------------------------------------------------- /canvascycle/palette.js: -------------------------------------------------------------------------------- 1 | // 8-bit Palette Classes for use in HTML5 Canvas 2 | // Ported from a C++ library written by Joseph Huckaby 3 | // BlendShift Technology conceived, designed and coded by Joseph Huckaby 4 | // Copyright (c) 2001 - 2024 Joseph Huckaby and PixlCore. 5 | // MIT Licensed: https://github.com/jhuckaby/canvascycle/blob/main/LICENSE.md 6 | 7 | Class.create( 'Color', { 8 | // represents one 24-bit RGB color 9 | red: 0, 10 | green: 0, 11 | blue: 0, 12 | 13 | __construct: function(r, g, b) { 14 | this.set(r, g, b); 15 | }, 16 | 17 | set: function(r, g, b) { 18 | this.red = r; 19 | this.green = g; 20 | this.blue = b; 21 | } 22 | } ); 23 | 24 | Class.create( 'Cycle', { 25 | // represents one cycle (one range of colors that animate) 26 | rate: 0, 27 | reverse: 0, 28 | low: 0, 29 | high: 0, 30 | 31 | __construct: function(r, rev, l, h) { 32 | this.rate = r; 33 | this.reverse = rev; 34 | this.low = l; 35 | this.high = h; 36 | } 37 | } ); 38 | 39 | Class.create( 'Palette', { 40 | // represents a single palette, which can have 41 | // multiple "cycles" (animated color ranges) defined. 42 | 43 | colors: null, 44 | baseColors: null, 45 | cycles: null, 46 | numColors: 0, 47 | numCycles: 0, 48 | 49 | __static: { 50 | // static class members 51 | PRECISION: 100, 52 | USE_BLEND_SHIFT: 1, 53 | CYCLE_SPEED: 280, 54 | ENABLE_CYCLING: 1, 55 | 56 | // this utility function allows for variable precision floating point modulus 57 | DFLOAT_MOD: function(a,b) { return (Math.floor(a*Palette.PRECISION) % Math.floor(b*Palette.PRECISION))/Palette.PRECISION; } 58 | }, 59 | 60 | __construct: function(clrs, cycls) { 61 | // class constructor 62 | this.colors = []; 63 | this.baseColors = []; 64 | for (var idx = 0, len = clrs.length; idx < len; idx++) { 65 | var clr = clrs[idx]; 66 | this.baseColors.push( new Color( clr[0], clr[1], clr[2] ) ); 67 | } 68 | 69 | this.cycles = []; 70 | for (var idx = 0, len = cycls.length; idx < len; idx++) { 71 | var cyc = cycls[idx]; 72 | this.cycles.push( new Cycle( cyc.rate, cyc.reverse, cyc.low, cyc.high ) ); 73 | } 74 | 75 | this.numColors = this.baseColors.length; 76 | this.numCycles = this.cycles.length; 77 | }, 78 | 79 | copyColors: function(source, dest) { 80 | // copy one array of colors to another 81 | for (var idx = 0, len = source.length; idx < len; idx++) { 82 | if (!dest[idx]) dest[idx] = new Color(); 83 | dest[idx].red = source[idx].red; 84 | dest[idx].green = source[idx].green; 85 | dest[idx].blue = source[idx].blue; 86 | } 87 | }, 88 | 89 | swapColors: function(a, b) { 90 | // swap the color values of a with b 91 | var temp; 92 | temp = a.red; a.red = b.red; b.red = temp; 93 | temp = a.green; a.green = b.green; b.green = temp; 94 | temp = a.blue; a.blue = b.blue; b.blue = temp; 95 | }, 96 | 97 | reverseColors: function(colors, range) { 98 | // reverse order of colors 99 | var i; 100 | var cycleSize = (range.high - range.low) + 1; 101 | 102 | for (i=0; i max) frame = max; 113 | 114 | tempColor.red = Math.floor( sourceColor.red + (((destColor.red - sourceColor.red) * frame) / max) ); 115 | tempColor.green = Math.floor( sourceColor.green + (((destColor.green - sourceColor.green) * frame) / max) ); 116 | tempColor.blue = Math.floor( sourceColor.blue + (((destColor.blue - sourceColor.blue) * frame) / max) ); 117 | 118 | return(tempColor); 119 | }, 120 | 121 | shiftColors: function(colors, range, amount) { 122 | // shift (hard cycle) colors by amount 123 | var i, j, temp; 124 | amount = Math.floor(amount); 125 | 126 | for (i = 0; i < amount; i++) { 127 | temp = colors[range.high]; 128 | for (j=range.high-1; j>=range.low; j--) 129 | colors[j+1] = colors[j]; 130 | colors[range.low] = temp; 131 | } // i loop 132 | }, 133 | 134 | blendShiftColors: function(colors, range, amount) { 135 | // shift colors using BlendShift (fade colors creating a smooth transition) 136 | // BlendShift Technology conceived, designed and coded by Joseph Huckaby 137 | var j, temp; 138 | 139 | this.shiftColors(colors, range, amount); 140 | 141 | var frame = Math.floor( (amount - Math.floor(amount)) * Palette.PRECISION ); 142 | 143 | temp = colors[range.high]; 144 | for (j=range.high-1; j>=range.low; j--) 145 | colors[j+1] = this.fadeColor(colors[j+1], colors[j], frame, Palette.PRECISION); 146 | colors[range.low] = this.fadeColor(colors[range.low], temp, frame, Palette.PRECISION); 147 | }, 148 | 149 | cycle: function(sourceColors, timeNow, speedAdjust, blendShift) { 150 | // cycle all animated color ranges in palette based on timestamp 151 | var i; 152 | var cycleSize, cycleRate; 153 | var cycleAmount; 154 | 155 | this.copyColors( sourceColors, this.colors ); 156 | 157 | if (Palette.ENABLE_CYCLING) { 158 | for (i=0; i= cycleSize) cycleAmount = (cycleSize*2) - cycleAmount; 172 | } 173 | else if (cycle.reverse < 6) { 174 | // sine wave 175 | cycleAmount = DFLOAT_MOD((timeNow / (1000 / cycleRate)), cycleSize); 176 | cycleAmount = Math.sin((cycleAmount * 3.1415926 * 2)/cycleSize) + 1; 177 | if (cycle.reverse == 4) cycleAmount *= (cycleSize / 4); 178 | else if (cycle.reverse == 5) cycleAmount *= (cycleSize / 2); 179 | } 180 | 181 | if (cycle.reverse == 2) this.reverseColors(this.colors, cycle); 182 | 183 | if (Palette.USE_BLEND_SHIFT && blendShift) this.blendShiftColors(this.colors, cycle, cycleAmount); 184 | else this.shiftColors(this.colors, cycle, cycleAmount); 185 | 186 | if (cycle.reverse == 2) this.reverseColors(this.colors, cycle); 187 | 188 | cycle.cycleAmount = cycleAmount; 189 | } // active cycle 190 | } // i loop 191 | } 192 | }, 193 | 194 | fade: function(destPalette, frame, max) { 195 | // fade entire palette to another, by adjustable amount 196 | var idx; 197 | 198 | for (idx=0; idx 7 | #include 8 | #include 9 | #include 10 | 11 | #include "palette.h" 12 | 13 | #define PRECISION 100 14 | #define DFLOAT_MOD(a,b) (double)((long)(a*PRECISION) % (b*PRECISION))/PRECISION 15 | 16 | char USE_BLEND_SHIFT = 1; 17 | short CYCLE_SPEED = 280; 18 | char ENABLE_CYCLING = 1; 19 | 20 | void copyColors(register color_register_32 *source_colors, register color_register_32 *dest_colors) { 21 | register unsigned short i; 22 | 23 | for (i=0; i<16; i++) { 24 | *(dest_colors++) = *(source_colors++); *(dest_colors++) = *(source_colors++); 25 | *(dest_colors++) = *(source_colors++); *(dest_colors++) = *(source_colors++); 26 | 27 | *(dest_colors++) = *(source_colors++); *(dest_colors++) = *(source_colors++); 28 | *(dest_colors++) = *(source_colors++); *(dest_colors++) = *(source_colors++); 29 | 30 | *(dest_colors++) = *(source_colors++); *(dest_colors++) = *(source_colors++); 31 | *(dest_colors++) = *(source_colors++); *(dest_colors++) = *(source_colors++); 32 | 33 | *(dest_colors++) = *(source_colors++); *(dest_colors++) = *(source_colors++); 34 | *(dest_colors++) = *(source_colors++); *(dest_colors++) = *(source_colors++); 35 | } 36 | } 37 | 38 | color_register_32 fadeColor(color_register_32 sourceColor, color_register_32 destColor, long frame, long max) { 39 | color_register_32 tempColor; 40 | 41 | if (!max) return sourceColor; // avoid divide by zero 42 | if (frame < 0) frame = 0; 43 | if (frame > max) frame = max; 44 | 45 | tempColor.red = sourceColor.red + (((long)(destColor.red - sourceColor.red) * frame) / max); 46 | tempColor.green = sourceColor.green + (((long)(destColor.green - sourceColor.green) * frame) / max); 47 | tempColor.blue = sourceColor.blue + (((long)(destColor.blue - sourceColor.blue) * frame) / max); 48 | 49 | return(tempColor); 50 | } 51 | 52 | color_register_32 fadeColor(color_register_32 sourceColor, color_register_32 destColor, short frame, short max) { 53 | return fadeColor(sourceColor, destColor, (long)frame, (long)max); 54 | } 55 | 56 | void shiftColors(color_register_32 *colors, c_range *range, short amount) { 57 | short i,j; 58 | color_register_32 temp; 59 | 60 | for (i = 0; i < amount; i++) { 61 | temp = colors[range->high]; 62 | for (j=range->high-1; j>=range->low; j--) 63 | colors[j+1] = colors[j]; 64 | colors[range->low] = temp; 65 | } // i loop 66 | } 67 | 68 | void blendShiftColors(color_register_32 *colors, c_range *range, double amount) { 69 | short j; 70 | color_register_32 temp; 71 | 72 | shiftColors(colors, range, (short)amount); 73 | 74 | short frame = (short)( (double)((double)amount - (short)amount) * PRECISION ); 75 | 76 | temp = colors[range->high]; 77 | for (j=range->high-1; j>=range->low; j--) 78 | colors[j+1] = fadeColor(colors[j+1], colors[j], frame, PRECISION); 79 | colors[range->low] = fadeColor(colors[range->low], temp, frame, PRECISION); 80 | } 81 | 82 | color_register_32 setRGB(unsigned char red, unsigned char green, unsigned char blue) { 83 | color_register_32 temp; 84 | 85 | temp.red = red; 86 | temp.green = green; 87 | temp.blue = blue; 88 | temp.padding = 0; 89 | 90 | return(temp); 91 | } 92 | 93 | char colorEqual(color_register_32 a, color_register_32 b) { 94 | return (a.red==b.red && a.green==b.green && a.blue==b.blue); 95 | } 96 | 97 | char hasPink(color_register_32 *colors, c_range *range) { 98 | short i; 99 | 100 | for (i=range->low; i<=range->high; i++) 101 | if (colors[i].red==255 && colors[i].green==0 && colors[i].blue==255) return 1; 102 | 103 | return 0; 104 | } 105 | 106 | void swapColors(color_register_32 *a, color_register_32 *b) { 107 | color_register_32 temp; 108 | 109 | temp = *a; *a = *b; *b = temp; 110 | } 111 | 112 | void reverseColors(color_register_32 *colors, c_range *range) { 113 | short i; 114 | short cycleSize = (range->high - range->low) + 1; 115 | 116 | for (i=0; ilow+i], &colors[range->high-i]); 118 | } 119 | 120 | unsigned char findColorIndex(color_register_32 *colors, color_register_32 color) { 121 | short i; 122 | 123 | for (i=0; icolors = (color_register_32 *)malloc(sizeof(color_register_32) * MAX_COLORS); 138 | this->baseColors = (color_register_32 *)malloc(sizeof(color_register_32) * MAX_COLORS); 139 | 140 | this->numColors = MAX_COLORS; 141 | this->numCycles = 0; 142 | 143 | this->cycles = NULL; 144 | } 145 | 146 | Palette::~Palette() { 147 | if (this->colors) free(this->colors); 148 | if (this->baseColors) free(this->baseColors); 149 | if (this->cycles) free(this->cycles); 150 | } 151 | 152 | void Palette::setupCycles() { 153 | if (!this->cycles) this->cycles = (c_range *)malloc(sizeof(c_range) * MAX_CYCLES); 154 | } 155 | 156 | void Palette::cycle(color_register_32 *sourceColors, unsigned long timeNow, float speedAdjust, char blendShift) { 157 | short i; 158 | short cycleSize, cycleRate; 159 | double cycleAmount; 160 | 161 | copyColors(sourceColors, this->colors); 162 | 163 | if (ENABLE_CYCLING) { 164 | for (i=0; inumCycles; i++) { 165 | if (this->cycles[i].rate) { 166 | cycleSize = (this->cycles[i].high - this->cycles[i].low) + 1; 167 | cycleRate = this->cycles[i].rate / (short)((float)CYCLE_SPEED * speedAdjust); 168 | 169 | if (this->cycles[i].reverse < 3) 170 | cycleAmount = DFLOAT_MOD((double)(timeNow / (1000 / (double)cycleRate)), cycleSize); 171 | else if (this->cycles[i].reverse == 3) { 172 | cycleAmount = DFLOAT_MOD((double)(timeNow / (1000 / (double)cycleRate)), cycleSize * 2); 173 | if (cycleAmount >= cycleSize) cycleAmount = (cycleSize*2) - cycleAmount; 174 | } else if (this->cycles[i].reverse < 6) { 175 | cycleAmount = DFLOAT_MOD((double)(timeNow / (1000 / (double)cycleRate)), cycleSize); 176 | cycleAmount = sin((cycleAmount * 3.1415926 * 2)/cycleSize) + 1; 177 | if (this->cycles[i].reverse == 4) cycleAmount *= (cycleSize / 4); 178 | else if (this->cycles[i].reverse == 5) cycleAmount *= (cycleSize / 2); 179 | } 180 | 181 | if (this->cycles[i].reverse == 2) reverseColors(this->colors, &this->cycles[i]); 182 | 183 | if (USE_BLEND_SHIFT && blendShift && !hasPink(this->colors, &this->cycles[i])) blendShiftColors(this->colors, &this->cycles[i], cycleAmount); 184 | else shiftColors(this->colors, &this->cycles[i], (short)cycleAmount); 185 | 186 | if (this->cycles[i].reverse == 2) reverseColors(this->colors, &this->cycles[i]); 187 | } // active cycle 188 | } // i loop 189 | } 190 | } 191 | 192 | void Palette::fade(color_register_32 destColor, short frame, short max) { 193 | unsigned short index; 194 | 195 | for (index=0; index<256; index++) 196 | this->colors[index] = fadeColor(this->colors[index], destColor, frame, max); 197 | } 198 | 199 | void Palette::fade(Palette *destPalette, short frame, short max) { 200 | unsigned short index; 201 | 202 | for (index=0; indexnumColors; index++) 203 | this->colors[index] = fadeColor(this->colors[index], destPalette->colors[index], frame, max); 204 | } 205 | 206 | void Palette::invert() { 207 | unsigned short index; 208 | 209 | for (index=0; indexnumColors; index++) { 210 | this->colors[index].red = 255 - this->colors[index].red; 211 | this->colors[index].green = 255 - this->colors[index].green; 212 | this->colors[index].blue = 255 - this->colors[index].blue; 213 | } 214 | } 215 | 216 | // r,g,b values are from 0 to 1 217 | // h = [0,360], s = [0,1], v = [0,1] 218 | // if s == 0, then h = -1 (undefined) 219 | 220 | void RGBtoHSV( float r, float g, float b, float *h, float *s, float *v ) 221 | { 222 | float min, max, delta; 223 | 224 | min = r; 225 | if (g < min) min = g; 226 | if (b < min) min = b; 227 | // min = MIN( r, g, b ); 228 | 229 | max = r; 230 | if (g > max) max = g; 231 | if (b > max) max = b; 232 | // max = MAX( r, g, b ); 233 | 234 | *v = max; // v 235 | delta = max - min; 236 | if( max != 0 ) *s = delta / max; // s 237 | else { *s = 0; *h = -1; return; } // r = g = b = 0 // s = 0, v is undefined 238 | if( r == max ) *h = ( g - b ) / delta; // between yellow & magenta 239 | else if( g == max ) *h = 2 + ( b - r ) / delta; // between cyan & yellow 240 | else *h = 4 + ( r - g ) / delta; // between magenta & cyan 241 | *h *= 60; // degrees 242 | if( *h < 0 ) *h += 360; 243 | } 244 | 245 | void HSVtoRGB( float *r, float *g, float *b, float h, float s, float v ) 246 | { 247 | int i; 248 | float f, p, q, t; 249 | if( s == 0 ) { *r = *g = *b = v; return; } // achromatic (grey) 250 | h /= 60; // sector 0 to 5 251 | i = int( h ); 252 | f = h - i; // factorial part of h 253 | p = v * ( 1 - s ); 254 | q = v * ( 1 - s * f ); 255 | t = v * ( 1 - s * ( 1 - f ) ); 256 | switch( i ) { 257 | case 0: *r = v; *g = t; *b = p; break; 258 | case 1: *r = q; *g = v; *b = p; break; 259 | case 2: *r = p; *g = v; *b = t; break; 260 | case 3: *r = p; *g = q; *b = v; break; 261 | case 4: *r = t; *g = p; *b = v; break; 262 | default: *r = v; *g = p; *b = q; break; 263 | } 264 | } 265 | 266 | color_register_32 RGBtoHSV( color_register_32 rgbColor ) { 267 | float h2, s2, v2; 268 | color_register_32 hsvColor; 269 | RGBtoHSV( (float)((float)rgbColor.red / 255.0), (float)((float)rgbColor.green / 255.0), (float)((float)rgbColor.blue / 255.0), &h2, &s2, &v2 ); 270 | hsvColor.red = (unsigned char)((float)((h2 * 256.0) / 360.0)); 271 | hsvColor.green = (unsigned char)((float)(s2 * 255.0)); 272 | hsvColor.blue = (unsigned char)((float)(v2 * 255.0)); 273 | return hsvColor; 274 | } 275 | 276 | color_register_32 HSVtoRGB( color_register_32 hsvColor ) { 277 | float r2, g2, b2; 278 | color_register_32 rgbColor; 279 | HSVtoRGB( &r2, &g2, &b2, (float)(((float)hsvColor.red * 360.0) / 256.0), (float)hsvColor.green / 255.0, (float)hsvColor.blue / 255.0 ); 280 | rgbColor.red = (unsigned char)((float)r2 * 255.0); 281 | rgbColor.green = (unsigned char)((float)g2 * 255.0); 282 | rgbColor.blue = (unsigned char)((float)b2 * 255.0); 283 | return rgbColor; 284 | } 285 | 286 | -------------------------------------------------------------------------------- /lbm2json/sprite.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * Library for loading DeluxePaint II LBM/BBM files. 3 | * Copyright 2001-2002 Joseph Huckaby. All rights reserved. 4 | **/ 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | #include "geometry.h" 11 | #include "palette.h" 12 | #include "bitmap.h" 13 | #include "sprite.h" 14 | 15 | color_register_32 black = setRGB(0, 0, 0); 16 | color_register_32 white = setRGB(255, 255, 255); 17 | 18 | short check_id(char *id, char *check) { 19 | if ((id[0] == check[0]) 20 | && (id[1] == check[1]) 21 | && (id[2] == check[2]) 22 | && (id[3] == check[3])) { 23 | return(1); 24 | } 25 | return(0); 26 | } 27 | 28 | void endian_swap(long *value) { 29 | unsigned char *chs; 30 | unsigned char temp; 31 | 32 | chs = (unsigned char *)value; 33 | 34 | temp = chs[0]; chs[0] = chs[3]; chs[3] = temp; 35 | temp = chs[1]; chs[1] = chs[2]; chs[2] = temp; 36 | } 37 | 38 | void endian_swap(unsigned long *value) { 39 | endian_swap((long *)value); 40 | } 41 | 42 | void endian_swap(short *value) { 43 | unsigned char *chs; 44 | unsigned char temp; 45 | 46 | chs = (unsigned char *)value; 47 | 48 | temp = chs[0]; chs[0] = chs[1]; chs[1] = temp; 49 | } 50 | 51 | void endian_swap(unsigned short *value) { 52 | endian_swap((short *)value); 53 | } 54 | 55 | Sprite::Sprite () { 56 | this->init(); 57 | } 58 | 59 | Sprite::Sprite (char *newFilename) { 60 | printf("Creating new bitmap from: %s\n", newFilename); 61 | this->init(); 62 | this->load_bitmap(newFilename); 63 | } 64 | 65 | Sprite::~Sprite () { 66 | if (this->bitmap) delete this->bitmap; 67 | if (this->alphaSprite) delete this->alphaSprite; 68 | 69 | if (this->sourceRect) delete this->sourceRect; 70 | } 71 | 72 | void Sprite::init () { 73 | this->x = 0; 74 | this->y = 0; 75 | this->visible = 1; 76 | this->error = 0; 77 | this->prepareBlits = 1; 78 | this->blendShift = 1; 79 | this->cycleSpeed = 1.0; 80 | this->isAlpha = 0; 81 | this->ditherAlpha = 0; 82 | this->priority = 1; 83 | this->lastFrameCount = 0; 84 | this->lastFrameTicks = 0; 85 | this->sourceRect = NULL; 86 | 87 | this->bitmap = NULL; 88 | this->alphaSprite = NULL; 89 | this->nextSprite = NULL; 90 | 91 | this->timeline = NULL; 92 | 93 | short i; 94 | for (i=0; i<256; i++) this->stencil[i] = 0; 95 | } 96 | 97 | void Sprite::load_bitmap(char *newFilename) { 98 | if (this->bitmap) { 99 | delete this->bitmap; 100 | this->bitmap = NULL; 101 | } 102 | 103 | this->bitmap = this->load_from_lbm(newFilename); 104 | if (this->bitmap) { 105 | this->sourceRect = new Rect(this->bitmap->bounds); 106 | 107 | } // bitmap loaded successfully 108 | } 109 | 110 | void Sprite::load_alpha(char *newFilename) { 111 | if (this->alphaSprite) { 112 | delete this->alphaSprite; 113 | this->alphaSprite = NULL; 114 | } 115 | 116 | this->alphaSprite = new Sprite(); 117 | this->alphaSprite->isAlpha = 1; 118 | this->alphaSprite->load_bitmap(newFilename); 119 | 120 | } 121 | 122 | Bitmap *Sprite::load_from_lbm (char *newFilename) { 123 | FILE* in; 124 | chunk mychunk; 125 | signed char sdata; 126 | int i, j; 127 | unsigned char *temp; 128 | unsigned long len, maxLen, bodyBytes; 129 | unsigned char udata; 130 | color_register *color_map = NULL; 131 | bitmap_header bmhd; 132 | Bitmap *newBitmap = NULL; 133 | // struct stat mystat; 134 | 135 | // stat(newFilename, &mystat); 136 | 137 | in = (FILE*)fopen(newFilename, "rb"); 138 | if (!in) { 139 | printf("Could not fopen file: %s\n", newFilename); 140 | this->error = 1; 141 | return NULL; 142 | } 143 | 144 | fread(&mychunk, sizeof(chunk), 1, in); 145 | fseek(in, 4, SEEK_CUR); // skip over 'PBM ' 146 | 147 | if (!check_id(mychunk.id, "FORM")) { 148 | printf("FORM chunk not found.\n"); 149 | this->error = 1; 150 | fclose(in); 151 | return NULL; 152 | } 153 | 154 | endian_swap(&mychunk.length); 155 | // printf("rest of file: %d\n",mychunk.length); 156 | 157 | while (!feof(in)) { 158 | mychunk.id[0] = ' '; 159 | mychunk.id[1] = ' '; 160 | mychunk.id[2] = ' '; 161 | mychunk.id[3] = ' '; 162 | fread(&mychunk, sizeof(chunk), 1, in); 163 | endian_swap(&mychunk.length); 164 | 165 | printf("\nChunk: %c%c%c%c (%d)\n", mychunk.id[0], mychunk.id[1], mychunk.id[2], mychunk.id[3], mychunk.length); 166 | 167 | if (check_id(mychunk.id, "BMHD")) { 168 | fread(&bmhd, sizeof(bitmap_header), 1, in); 169 | 170 | endian_swap(&bmhd.width); 171 | endian_swap(&bmhd.height); 172 | 173 | bmhd.width += (2-(bmhd.width%2))%2; // even widths only (round up) 174 | 175 | printf("\n"); 176 | printf("Bitmap Header found.\n"); 177 | printf("Width: %d\n", bmhd.width); 178 | printf("Height: %d\n", bmhd.height); 179 | printf("Compression: %d\n", bmhd.compression); 180 | printf("Planes: %d\n", bmhd.num_planes); 181 | printf("Masking: %d\n", bmhd.masking); 182 | 183 | if (bmhd.masking == 1) { 184 | printf("ERROR: bmhd.masking == 1 -- MASK INTERLEAVED WITH BODY PIXELS -- I cannot figure this out.\n"); 185 | this->error = 2; 186 | fclose(in); 187 | return NULL; 188 | } 189 | 190 | newBitmap = new Bitmap(bmhd.width, bmhd.height); 191 | strcpy(newBitmap->filename, newFilename); 192 | 193 | if (bmhd.masking == 2) { 194 | newBitmap->blackMask = 1; 195 | } 196 | 197 | } // BMHD 198 | 199 | else if (check_id(mychunk.id, "CMAP")) { 200 | if (!color_map) color_map = (color_register *)malloc(mychunk.length); 201 | fread(color_map, mychunk.length, 1, in); 202 | 203 | newBitmap->palette->numColors = mychunk.length/sizeof(color_register); 204 | 205 | for (i=0; ipalette->numColors; i++) { 206 | newBitmap->palette->baseColors[i].red = color_map[i].red; 207 | newBitmap->palette->baseColors[i].green = color_map[i].green; 208 | newBitmap->palette->baseColors[i].blue = color_map[i].blue; 209 | newBitmap->palette->baseColors[i].padding = 0; 210 | 211 | if (color_map[i].red == 255 && color_map[i].green == 0 && color_map[i].blue == 255) 212 | newBitmap->pinkMask = 1; 213 | } 214 | 215 | printf("\n"); 216 | printf("Palette found. %d color entries\n", newBitmap->palette->numColors); 217 | printf("Color 000: %d, %d, %d\n", color_map[0].red, color_map[0].green, color_map[0].blue); 218 | printf("Color 001: %d, %d, %d\n", color_map[1].red, color_map[1].green, color_map[1].blue); 219 | } 220 | 221 | else if (check_id(mychunk.id, "CRNG")) { 222 | if (!newBitmap->palette->numCycles) { 223 | newBitmap->palette->setupCycles(); 224 | printf("\n"); 225 | printf("Color cycling info found.\n"); 226 | } 227 | 228 | fread(&newBitmap->palette->cycles[newBitmap->palette->numCycles], mychunk.length, 1, in); 229 | 230 | endian_swap(&newBitmap->palette->cycles[newBitmap->palette->numCycles].rate); 231 | endian_swap(&newBitmap->palette->cycles[newBitmap->palette->numCycles].reverse); 232 | 233 | printf("Cycle %d: Reverse: %d Rate: %d Range: %d-%d\n", newBitmap->palette->numCycles, newBitmap->palette->cycles[newBitmap->palette->numCycles].reverse, newBitmap->palette->cycles[newBitmap->palette->numCycles].rate, newBitmap->palette->cycles[newBitmap->palette->numCycles].low, newBitmap->palette->cycles[newBitmap->palette->numCycles].high); 234 | 235 | newBitmap->palette->numCycles++; 236 | } // CRNG 237 | 238 | else if (check_id(mychunk.id, "BODY")) { 239 | printf("\n"); 240 | printf("Found body. Length: %d\n", mychunk.length); 241 | 242 | maxLen = (unsigned long)((unsigned long)newBitmap->width * (unsigned long)newBitmap->height); 243 | bodyBytes = 0; 244 | 245 | // int maskReadMode = 1; 246 | // int maskReadCounter = newBitmap->width; 247 | 248 | if (bmhd.compression) { 249 | temp = newBitmap->baseAddr; 250 | len = mychunk.length; 251 | sdata=fgetc(in); 252 | while( len>0 && !feof(in)) { 253 | len--; 254 | /* ByteRun1 decompression */ 255 | 256 | /* [0..127] : followed by n+1 bytes of data. */ 257 | if (sdata>=0) { 258 | i=sdata+1; 259 | for(j=0; jbaseAddr < maxLen - 1) temp++; 267 | bodyBytes++; 268 | maskReadCounter--; 269 | if (!maskReadCounter) { 270 | maskReadMode = 0; 271 | maskReadCounter = newBitmap->width / 8; 272 | } 273 | } 274 | else { 275 | maskReadCounter--; 276 | if (!maskReadCounter) { 277 | maskReadMode = 1; 278 | maskReadCounter = newBitmap->width; 279 | } 280 | } 281 | } // masking 282 | else { */ 283 | *temp = udata; 284 | if (temp - newBitmap->baseAddr < maxLen - 1) temp++; 285 | bodyBytes++; 286 | // } // no mask 287 | } 288 | } 289 | } 290 | /* [-1..-127] : followed by byte to be repeated (-n)+1 times*/ 291 | else if (sdata<=-1 && sdata>=-127) { 292 | i=(-sdata)+1; 293 | udata=fgetc(in); 294 | len--; 295 | for(j=0; jbaseAddr < maxLen - 1) temp++; 300 | bodyBytes++; 301 | maskReadCounter--; 302 | if (!maskReadCounter) { 303 | maskReadMode = 0; 304 | maskReadCounter = newBitmap->width / 8; 305 | } 306 | } 307 | else { 308 | maskReadCounter--; 309 | if (!maskReadCounter) { 310 | maskReadMode = 1; 311 | maskReadCounter = newBitmap->width; 312 | } 313 | } 314 | } // masking 315 | else { */ 316 | *temp = udata; 317 | if (temp - newBitmap->baseAddr < maxLen - 1) temp++; 318 | bodyBytes++; 319 | // } // no mask 320 | } 321 | } 322 | /* -128 : NOOP. */ 323 | 324 | sdata=fgetc(in); 325 | } 326 | } else { 327 | fread(newBitmap->baseAddr, maxLen, 1, in); 328 | bodyBytes += maxLen; 329 | } // no compression 330 | 331 | printf("Uncompressed length: %d\n", bodyBytes); 332 | printf("Should be: %d x %d = %d\n", newBitmap->width, newBitmap->height, maxLen); 333 | // printf("Pixel 0x0: %d\n", newBitmap->baseAddr[0]); 334 | // printf("Pixel 0x1: %d\n", newBitmap->baseAddr[640]); 335 | } // BODY 336 | 337 | else if (check_id(mychunk.id, "GRAB")) { 338 | printf("Found GRAB -- setting blackMask.\n"); 339 | if (mychunk.length>0) fseek(in, mychunk.length, SEEK_CUR); 340 | // newBitmap->blackMask = 1; 341 | } 342 | 343 | else { 344 | // printf("\nUnknown chunk: %c%c%c%c, length: %d\n", mychunk.id[0], mychunk.id[1], mychunk.id[2], mychunk.id[3], mychunk.length); 345 | if (mychunk.length>0) fseek(in, mychunk.length, SEEK_CUR); 346 | } // unknown chunk 347 | 348 | if (mychunk.length % 2 != 0) 349 | fseek(in, 2 - (mychunk.length%2), SEEK_CUR); 350 | } // while !eof 351 | 352 | fclose(in); 353 | 354 | if (color_map) free(color_map); 355 | 356 | if (!newBitmap) this->error = 1; 357 | // else newBitmap->modDate = (unsigned long)mystat.st_mtime; 358 | 359 | return newBitmap; 360 | } 361 | 362 | -------------------------------------------------------------------------------- /canvascycle/main.js: -------------------------------------------------------------------------------- 1 | // Color Cycling in HTML5 Canvas 2 | // BlendShift Technology conceived, designed and coded by Joseph Huckaby 3 | // Copyright (c) 2010 - 2024 Joseph Huckaby and PixlCore. 4 | // MIT Licensed: https://github.com/jhuckaby/canvascycle/blob/main/LICENSE.md 5 | 6 | FrameCount.visible = false; 7 | 8 | var CanvasCycle = { 9 | 10 | cookie: new CookieTree(), 11 | ctx: null, 12 | imageData: null, 13 | clock: 0, 14 | inGame: false, 15 | bmp: null, 16 | globalTimeStart: (new Date()).getTime(), 17 | inited: false, 18 | optTween: null, 19 | winSize: null, 20 | globalBrightness: 1.0, 21 | lastBrightness: 0, 22 | sceneIdx: -1, 23 | highlightColor: -1, 24 | defaultMaxVolume: 0.5, 25 | 26 | settings: { 27 | showOptions: false, 28 | targetFPS: 60, 29 | zoomFull: false, 30 | blendShiftEnabled: true, 31 | speedAdjust: 1.0, 32 | sound: true 33 | }, 34 | 35 | contentSize: { 36 | width: 640, 37 | optionsWidth: 0, 38 | height: 480 + 40, 39 | scale: 1.0 40 | }, 41 | 42 | init: function() { 43 | // called when DOM is ready 44 | if (!this.inited) { 45 | this.inited = true; 46 | $('container').style.display = 'block'; 47 | $('d_options').style.display = 'none'; 48 | 49 | FrameCount.init(); 50 | this.handleResize(); 51 | 52 | var pal_disp = $('palette_display'); 53 | for (var idx = 0, len = 256; idx < len; idx++) { 54 | var div = document.createElement('div'); 55 | div._idx = idx; 56 | div.id = 'pal_' + idx; 57 | div.className = 'palette_color'; 58 | div.onmouseover = function() { CanvasCycle.highlightColor = this._idx; }; 59 | div.onmouseout = function() { CanvasCycle.highlightColor = -1; }; 60 | pal_disp.appendChild( div ); 61 | } 62 | var div = document.createElement('div'); 63 | div.className = 'clear'; 64 | pal_disp.appendChild( div ); 65 | 66 | // pick starting scene 67 | // var initialSceneIdx = Math.floor( Math.random() * scenes.length ); 68 | var initialSceneIdx = 0; 69 | 70 | // populate scene menu 71 | var html = ''; 72 | html += ''; 78 | $('d_scene_selector').innerHTML = html; 79 | 80 | // read prefs from cookie 81 | var prefs = this.cookie.get('settings'); 82 | if (prefs) { 83 | if (prefs.showOptions) this.toggleOptions(); 84 | this.setRate( prefs.targetFPS ); 85 | this.setZoom( prefs.zoomFull ); 86 | this.setSpeed( prefs.speedAdjust ); 87 | this.setBlendShift( prefs.blendShiftEnabled ); 88 | this.setSound( prefs.sound ); 89 | } 90 | 91 | // allow query to control sound 92 | if (location.href.match(/\bsound\=(\d+)/)) { 93 | this.setSound( !!parseInt(RegExp.$1, 10) ); 94 | } 95 | 96 | this.loadImage( scenes[initialSceneIdx].name ); 97 | this.sceneIdx = initialSceneIdx; 98 | } 99 | }, 100 | 101 | jumpScene: function(dir) { 102 | // next or prev scene 103 | this.sceneIdx += dir; 104 | if (this.sceneIdx >= scenes.length) this.sceneIdx = 0; 105 | else if (this.sceneIdx < 0) this.sceneIdx = scenes.length - 1; 106 | $('fe_scene').selectedIndex = this.sceneIdx; 107 | this.switchScene( $('fe_scene') ); 108 | }, 109 | 110 | switchScene: function(menu) { 111 | // switch to new scene (grab menu selection) 112 | this.stopSceneAudio(); 113 | 114 | var name = menu.options[menu.selectedIndex].value; 115 | this.sceneIdx = menu.selectedIndex; 116 | 117 | if (ua.mobile) { 118 | // no transitions on mobile devices, just switch as fast as possible 119 | this.inGame = false; 120 | 121 | this.ctx.clearRect(0, 0, this.bmp.width, this.bmp.height); 122 | this.ctx.fillStyle = "rgb(0,0,0)"; 123 | this.ctx.fillRect (0, 0, this.bmp.width, this.bmp.height); 124 | 125 | CanvasCycle.globalBrightness = 1.0; 126 | CanvasCycle.loadImage( name ); 127 | } 128 | else { 129 | TweenManager.removeAll({ category: 'scenefade' }); 130 | TweenManager.tween({ 131 | target: { value: this.globalBrightness, newSceneName: name }, 132 | duration: Math.floor( this.settings.targetFPS / 2 ), 133 | mode: 'EaseInOut', 134 | algo: 'Quadratic', 135 | props: { value: 0.0 }, 136 | onTweenUpdate: function(tween) { 137 | CanvasCycle.globalBrightness = tween.target.value; 138 | }, 139 | onTweenComplete: function(tween) { 140 | CanvasCycle.loadImage( tween.target.newSceneName ); 141 | }, 142 | category: 'scenefade' 143 | }); 144 | } 145 | }, 146 | 147 | loadImage: function(name) { 148 | // load image JSON from the server 149 | this.stop(); 150 | this.showLoading(); 151 | 152 | var url = 'image.php?file='+name+'&callback=CanvasCycle.processImage'; 153 | var scr = document.createElement('SCRIPT'); 154 | scr.type = 'text/javascript'; 155 | scr.src = url; 156 | document.getElementsByTagName('HEAD')[0].appendChild(scr); 157 | }, 158 | 159 | showLoading: function() { 160 | // show spinning loading indicator 161 | var loading = $('d_loading'); 162 | loading.style.left = '' + Math.floor( ((this.contentSize.width * this.contentSize.scale) / 2) - 16 ) + 'px'; 163 | loading.style.top = '' + Math.floor( ((this.contentSize.height * this.contentSize.scale) / 2) - 16 ) + 'px'; 164 | loading.show(); 165 | }, 166 | 167 | hideLoading: function() { 168 | // hide spinning loading indicator 169 | $('d_loading').hide(); 170 | }, 171 | 172 | processImage: function(img) { 173 | // initialize, receive image data from server 174 | this.bmp = new Bitmap(img); 175 | this.bmp.optimize(); 176 | 177 | // $('d_debug').innerHTML = img.filename; 178 | 179 | var canvas = $('mycanvas'); 180 | if (!canvas.getContext) return; // no canvas support 181 | 182 | if (!this.ctx) this.ctx = canvas.getContext('2d'); 183 | this.ctx.clearRect(0, 0, this.bmp.width, this.bmp.height); 184 | this.ctx.fillStyle = "rgb(0,0,0)"; 185 | this.ctx.fillRect (0, 0, this.bmp.width, this.bmp.height); 186 | 187 | if (!this.imageData) { 188 | if (this.ctx.createImageData) { 189 | this.imageData = this.ctx.createImageData( this.bmp.width, this.bmp.height ); 190 | } 191 | else if (this.ctx.getImageData) { 192 | this.imageData = this.ctx.getImageData( 0, 0, this.bmp.width, this.bmp.height ); 193 | } 194 | else return; // no canvas data support 195 | } 196 | 197 | if (ua.mobile) { 198 | // no transition on mobile devices 199 | this.globalBrightness = 1.0; 200 | } 201 | else { 202 | this.globalBrightness = 0.0; 203 | TweenManager.removeAll({ category: 'scenefade' }); 204 | TweenManager.tween({ 205 | target: { value: 0 }, 206 | duration: Math.floor( this.settings.targetFPS / 2 ), 207 | mode: 'EaseInOut', 208 | algo: 'Quadratic', 209 | props: { value: 1.0 }, 210 | onTweenUpdate: function(tween) { 211 | CanvasCycle.globalBrightness = tween.target.value; 212 | }, 213 | category: 'scenefade' 214 | }); 215 | } 216 | 217 | this.startSceneAudio(); 218 | }, 219 | 220 | run: function () { 221 | // start main loop 222 | if (!this.inGame) { 223 | this.inGame = true; 224 | this.animate(); 225 | } 226 | }, 227 | 228 | stop: function() { 229 | // stop main loop 230 | this.inGame = false; 231 | }, 232 | 233 | animate: function() { 234 | // animate one frame. and schedule next 235 | if (this.inGame) { 236 | var colors = this.bmp.palette.colors; 237 | 238 | if (this.settings.showOptions) { 239 | for (var idx = 0, len = colors.length; idx < len; idx++) { 240 | var clr = colors[idx]; 241 | var div = $('pal_'+idx); 242 | div.style.backgroundColor = 'rgb(' + clr.red + ',' + clr.green + ',' + clr.blue + ')'; 243 | } 244 | 245 | // if (this.clock % this.settings.targetFPS == 0) $('d_debug').innerHTML = 'FPS: ' + FrameCount.current; 246 | $('d_debug').innerHTML = 'FPS: ' + FrameCount.current + ((this.highlightColor != -1) ? (' - Color #' + this.highlightColor) : ''); 247 | } 248 | 249 | this.bmp.palette.cycle( this.bmp.palette.baseColors, GetTickCount(), this.settings.speedAdjust, this.settings.blendShiftEnabled ); 250 | if (this.highlightColor > -1) { 251 | this.bmp.palette.colors[ this.highlightColor ] = new Color(255, 255, 255); 252 | } 253 | if (this.globalBrightness < 1.0) { 254 | // bmp.palette.fadeToColor( pureBlack, 1.0 - globalBrightness, 1.0 ); 255 | this.bmp.palette.burnOut( 1.0 - this.globalBrightness, 1.0 ); 256 | } 257 | this.bmp.render( this.imageData, (this.lastBrightness == this.globalBrightness) && (this.highlightColor == this.lastHighlightColor) ); 258 | this.lastBrightness = this.globalBrightness; 259 | this.lastHighlightColor = this.highlightColor; 260 | 261 | this.ctx.putImageData( this.imageData, 0, 0 ); 262 | 263 | TweenManager.logic( this.clock ); 264 | this.clock++; 265 | FrameCount.count(); 266 | this.scaleAnimate(); 267 | if (this.inGame) setTimeout( function() { CanvasCycle.animate(); }, 1000 / this.settings.targetFPS ); 268 | } 269 | }, 270 | 271 | scaleAnimate: function() { 272 | // handle scaling image up or down 273 | if (this.settings.zoomFull) { 274 | // scale up to full size 275 | var totalNativeWidth = this.contentSize.width + this.contentSize.optionsWidth; 276 | var maxScaleX = (this.winSize.width - 30) / totalNativeWidth; 277 | 278 | var totalNativeHeight = this.contentSize.height; 279 | var maxScaleY = (this.winSize.height - 30) / totalNativeHeight; 280 | 281 | var maxScale = Math.min( maxScaleX, maxScaleY ); 282 | 283 | if (this.contentSize.scale != maxScale) { 284 | this.contentSize.scale += ((maxScale - this.contentSize.scale) / 8); 285 | if (Math.abs(this.contentSize.scale - maxScale) < 0.001) this.contentSize.scale = maxScale; // close enough 286 | 287 | var sty = $('mycanvas').style; 288 | 289 | if (ua.webkit) sty.webkitTransform = 'translate3d(0px, 0px, 0px) scale('+this.contentSize.scale+')'; 290 | else if (ua.ff) sty.MozTransform = 'scale('+this.contentSize.scale+')'; 291 | else if (ua.op) sty.OTransform = 'scale('+this.contentSize.scale+')'; 292 | else sty.transform = 'scale('+this.contentSize.scale+')'; 293 | 294 | sty.marginRight = '' + Math.floor( (this.contentSize.width * this.contentSize.scale) - this.contentSize.width ) + 'px'; 295 | $('d_header').style.width = '' + Math.floor(this.contentSize.width * this.contentSize.scale) + 'px'; 296 | this.repositionContainer(); 297 | } 298 | } 299 | else { 300 | // scale back down to native 301 | if (this.contentSize.scale > 1.0) { 302 | this.contentSize.scale += ((1.0 - this.contentSize.scale) / 8); 303 | if (this.contentSize.scale < 1.001) this.contentSize.scale = 1.0; // close enough 304 | 305 | var sty = $('mycanvas').style; 306 | 307 | if (ua.webkit) sty.webkitTransform = 'translate3d(0px, 0px, 0px) scale('+this.contentSize.scale+')'; 308 | else if (ua.ff) sty.MozTransform = 'scale('+this.contentSize.scale+')'; 309 | else if (ua.op) sty.OTransform = 'scale('+this.contentSize.scale+')'; 310 | else sty.transform = 'scale('+this.contentSize.scale+')'; 311 | 312 | sty.marginRight = '' + Math.floor( (this.contentSize.width * this.contentSize.scale) - this.contentSize.width ) + 'px'; 313 | $('d_header').style.width = '' + Math.floor(this.contentSize.width * this.contentSize.scale) + 'px'; 314 | this.repositionContainer(); 315 | } 316 | } 317 | }, 318 | 319 | repositionContainer: function() { 320 | // reposition container element based on inner window size 321 | var div = $('container'); 322 | if (div) { 323 | this.winSize = getInnerWindowSize(); 324 | div.style.left = '' + Math.floor((this.winSize.width / 2) - (((this.contentSize.width * this.contentSize.scale) + this.contentSize.optionsWidth) / 2)) + 'px'; 325 | div.style.top = '' + Math.floor((this.winSize.height / 2) - ((this.contentSize.height * this.contentSize.scale) / 2)) + 'px'; 326 | } 327 | }, 328 | 329 | handleResize: function() { 330 | // called when window resizes 331 | this.repositionContainer(); 332 | if (this.settings.zoomFull) this.scaleAnimate(); 333 | }, 334 | 335 | saveSettings: function() { 336 | // save settings in cookie 337 | this.cookie.set( 'settings', this.settings ); 338 | this.cookie.save(); 339 | }, 340 | 341 | startSceneAudio: function() { 342 | // start audio for current scene, if applicable 343 | var scene = scenes[ this.sceneIdx ]; 344 | if (scene.sound && this.settings.sound && window.Audio) { 345 | if (this.audioTrack) { 346 | try { this.audioTrack.pause(); } catch(e) {;} 347 | } 348 | TweenManager.removeAll({ category: 'audio' }); 349 | 350 | var ext = (ua.ff || ua.op) ? 'ogg' : 'mp3'; 351 | var track = this.audioTrack = new Audio( 'audio/' + scene.sound + '.' + ext ); 352 | track.volume = 0; 353 | track.loop = true; 354 | track.autobuffer = false; 355 | track.autoplay = true; 356 | 357 | track.addEventListener('canplaythrough', function() { 358 | track.play(); 359 | TweenManager.tween({ 360 | target: track, 361 | duration: Math.floor( CanvasCycle.settings.targetFPS * 2 ), 362 | mode: 'EaseOut', 363 | algo: 'Linear', 364 | props: { volume: scene.maxVolume || CanvasCycle.defaultMaxVolume }, 365 | category: 'audio' 366 | }); 367 | CanvasCycle.hideLoading(); 368 | CanvasCycle.run(); 369 | }, false); 370 | 371 | if (ua.iphone || ua.ipad) { 372 | // these may support audio, but just don't invoke events 373 | // try to force it 374 | setTimeout( function() { 375 | track.play(); 376 | track.volume = 1.0; 377 | CanvasCycle.hideLoading(); 378 | CanvasCycle.run(); 379 | }, 1000 ); 380 | } 381 | 382 | if (ua.ff || ua.mobile) { 383 | // loop doesn't seem to work on FF or mobile devices, so let's force it 384 | track.addEventListener('ended', function() { 385 | track.currentTime = 0; 386 | track.play(); 387 | }, false); 388 | } 389 | 390 | track.load(); 391 | } // sound enabled and supported 392 | else { 393 | // no sound for whatever reason, so just start main loop 394 | this.hideLoading(); 395 | this.run(); 396 | } 397 | }, 398 | 399 | stopSceneAudio: function() { 400 | // fade out and stop audio for current scene 401 | var scene = scenes[ this.sceneIdx ]; 402 | if (scene.sound && this.settings.sound && window.Audio && this.audioTrack) { 403 | var track = this.audioTrack; 404 | 405 | if (ua.iphone || ua.ipad) { 406 | // no transition here, so just stop sound 407 | track.pause(); 408 | } 409 | else { 410 | TweenManager.removeAll({ category: 'audio' }); 411 | TweenManager.tween({ 412 | target: track, 413 | duration: Math.floor( CanvasCycle.settings.targetFPS / 2 ), 414 | mode: 'EaseOut', 415 | algo: 'Linear', 416 | props: { volume: 0 }, 417 | onTweenComplete: function(tween) { 418 | // ff has weird delay with volume fades, so allow sound to continue 419 | // will be stopped when next one starts 420 | if (!ua.ff) track.pause(); 421 | }, 422 | category: 'audio' 423 | }); 424 | } 425 | } 426 | }, 427 | 428 | toggleOptions: function() { 429 | var startValue, endValue; 430 | TweenManager.removeAll({ category: 'options' }); 431 | 432 | if (!this.settings.showOptions) { 433 | startValue = 0; 434 | if (this.optTween) startValue = this.optTween.target.value; 435 | endValue = 1.0; 436 | $('d_options').style.display = ''; 437 | $('d_options').style.opacity = startValue; 438 | $('btn_options_toggle').innerHTML = '« Hide Options'; 439 | } 440 | else { 441 | startValue = 1.0; 442 | if (this.optTween) startValue = this.optTween.target.value; 443 | endValue = 0; 444 | $('btn_options_toggle').innerHTML = 'Show Options »'; 445 | } 446 | 447 | this.optTween = TweenManager.tween({ 448 | target: { value: startValue }, 449 | duration: Math.floor( this.settings.targetFPS / 3 ), 450 | mode: 'EaseOut', 451 | algo: 'Quadratic', 452 | props: { value: endValue }, 453 | onTweenUpdate: function(tween) { 454 | // $('d_options').style.left = '' + Math.floor(tween.target.value - 150) + 'px'; 455 | $('d_options').style.opacity = tween.target.value; 456 | $('btn_options_toggle').style.left = '' + Math.floor(tween.target.value * 128) + 'px'; 457 | 458 | CanvasCycle.contentSize.optionsWidth = Math.floor( tween.target.value * 150 ); 459 | CanvasCycle.handleResize(); 460 | }, 461 | onTweenComplete: function(tween) { 462 | if (tween.target.value == 0) $('d_options').style.display = 'none'; 463 | CanvasCycle.optTween = null; 464 | }, 465 | category: 'options' 466 | }); 467 | 468 | this.settings.showOptions = !this.settings.showOptions; 469 | this.saveSettings(); 470 | }, 471 | 472 | setZoom: function(enabled) { 473 | if (enabled != this.settings.zoomFull) { 474 | this.settings.zoomFull = enabled; 475 | this.saveSettings(); 476 | $('btn_zoom_actual').setClass('selected', !enabled); 477 | $('btn_zoom_max').setClass('selected', enabled); 478 | } 479 | }, 480 | 481 | setSound: function(enabled) { 482 | $('btn_sound_on').setClass('selected', enabled); 483 | $('btn_sound_off').setClass('selected', !enabled); 484 | this.settings.sound = enabled; 485 | 486 | if (this.sceneIdx > -1) { 487 | if (enabled) { 488 | // enable sound 489 | if (this.audioTrack) this.audioTrack.play(); 490 | else this.startSceneAudio(); 491 | } 492 | else { 493 | // disable sound 494 | if (this.audioTrack) this.audioTrack.pause(); 495 | } 496 | } 497 | 498 | this.saveSettings(); 499 | }, 500 | 501 | setRate: function(rate) { 502 | /* $('btn_rate_30').setClass('selected', rate == 30); 503 | $('btn_rate_60').setClass('selected', rate == 60); 504 | $('btn_rate_90').setClass('selected', rate == 90); */ 505 | this.settings.targetFPS = rate; 506 | this.saveSettings(); 507 | }, 508 | 509 | setSpeed: function(speed) { 510 | $('btn_speed_025').setClass('selected', speed == 0.25); 511 | $('btn_speed_05').setClass('selected', speed == 0.5); 512 | $('btn_speed_1').setClass('selected', speed == 1); 513 | $('btn_speed_2').setClass('selected', speed == 2); 514 | $('btn_speed_4').setClass('selected', speed == 4); 515 | this.settings.speedAdjust = speed; 516 | this.saveSettings(); 517 | }, 518 | 519 | setBlendShift: function(enabled) { 520 | $('btn_blendshift_on').setClass('selected', enabled); 521 | $('btn_blendshift_off').setClass('selected', !enabled); 522 | this.settings.blendShiftEnabled = enabled; 523 | this.saveSettings(); 524 | } 525 | 526 | }; 527 | 528 | var CC = CanvasCycle; // shortcut 529 | --------------------------------------------------------------------------------