diff --git a/Readme.md b/Readme.md
index 86310a5..ba0402d 100644
--- a/Readme.md
+++ b/Readme.md
@@ -26,8 +26,9 @@ The app should work offline and perform well even on low-end devices. It's very
There's also an easy mode for kids (slower ball) and a color-blind mode (no color related game mechanics).
## Doing
-- publish on Fdroid
-
+- publish on Fdroid
+- enable gif export of gameplay capture
+- enable export of gameplay capture in webview
## Perk ideas
- wrap left / right
- n% of the broken bricks respawn when the ball touches the puck
@@ -67,7 +68,6 @@ There's also an easy mode for kids (slower ball) and a color-blind mode (no colo
- show total score on end screen (score added to total)
- show stats on end screen compared to other runs
- handle back bouton in menu
-- Make a small mp4/gif of game which can be shown on gameover and shared. https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder
- mouvement relatif du puck
- balls should collide with each other
- when game resumes near bottom, be unvulnerable for .5s ? , once per level
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 38507a4..e5cb440 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -11,8 +11,8 @@ android {
applicationId = "me.lecaro.breakout"
minSdk = 21
targetSdk = 34
- versionCode = 28999986
- versionName = "28999986"
+ versionCode = 29000794
+ versionName = "29000794"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary = true
diff --git a/app/src/main/assets/game.js b/app/src/main/assets/game.js
index 7802db3..8b218d5 100644
--- a/app/src/main/assets/game.js
+++ b/app/src/main/assets/game.js
@@ -107,6 +107,7 @@ function play() {
if (audioContext) {
audioContext.resume()
}
+ resumeRecording()
}
function pause() {
@@ -119,6 +120,7 @@ function pause() {
audioContext.suspend()
}, 1000)
}
+ pauseRecording()
}
let offsetX, offsetXRoundedDown, gameZoneWidth, gameZoneWidthRoundedUp, gameZoneHeight, brickWidth, needsRender = true;
@@ -353,7 +355,8 @@ async function openUpgradesPicker() {
while (repeats--) {
const actions = pickRandomUpgrades(choices);
if (!actions.length) break
- let textAfterButtons = `
Upgrades picked so far :
${pickedUpgradesHTMl()}
`;
+ let textAfterButtons = `Upgrades picked so far :
${pickedUpgradesHTMl()}
+
`;
const cb = await asyncAlert({
title: "Pick an upgrade " + (repeats ? "(" + (repeats + 1) + ")" : ""), actions, text, allowClose: false,
@@ -394,7 +397,8 @@ function setLevel(l) {
flashes = [];
background.src = 'data:image/svg+xml;base64,' + btoa(lvl.svg)
-
+ stopRecording()
+ startRecordingGame()
}
function currentLevelInfo() {
@@ -770,6 +774,8 @@ function restart() {
setLevel(0);
scoreStory.push(`You started playing with the upgrade "${upgrades.find(u => u.id === randomGift)?.name}" on the level "${runLevels[0].name}". `,);
+
+ pauseRecording()
}
function setMousePos(x) {
@@ -1041,7 +1047,7 @@ function tick() {
const windD = (puck - (offsetX + gameZoneWidth / 2)) / gameZoneWidth * 2 * perks.wind
for (var i = 0; i < perks.wind; i++) {
- if(Math.random()*Math.abs(windD)>0.5) {
+ if (Math.random() * Math.abs(windD) > 0.5) {
flashes.push({
type: "particle",
duration: 150,
@@ -1049,9 +1055,9 @@ function tick() {
time: levelTime,
size: coinSize / 2,
color: rainbowColor(),
- x: offsetXRoundedDown+ Math.random() * gameZoneWidthRoundedUp ,
+ x: offsetXRoundedDown + Math.random() * gameZoneWidthRoundedUp,
y: Math.random() * gameZoneHeight,
- vx: windD*8,
+ vx: windD * 8,
vy: 0,
});
}
@@ -1298,6 +1304,7 @@ function addToTotalScore(points) {
function gameOver(title, intro) {
if (!running) return;
pause()
+ stopRecording()
runStatistics.ended = Date.now()
@@ -1371,7 +1378,7 @@ function gameOver(title, intro) {
${intro}
${unlocksInfo}
`, textAfterButtons: `
-
+
${scoreStory.map((t) => "" + t + "
").join("")}
`
}).then(() => restart());
@@ -1737,8 +1744,9 @@ function render() {
ctx.fillStyle = puckColor;
ctx.globalCompositeOperation = "source-over";
if (offsetXRoundedDown) {
- ctx.fillRect(offsetX, 0, 1, height);
- ctx.fillRect(width - offsetX - 1, 0, 1, height);
+ // draw outside of gaming area to avoid capturing borders in recordings
+ ctx.fillRect(offsetX - 1, 0, 1, height);
+ ctx.fillRect(width - offsetX + 1, 0, 1, height);
}
if (isSettingOn("mobile-mode")) {
ctx.fillRect(offsetXRoundedDown, gameZoneHeight, gameZoneWidthRoundedUp, 1);
@@ -1752,6 +1760,8 @@ function render() {
if (shaked) {
ctx.resetTransform();
}
+
+ recordOneFrame()
}
let cachedBricksRender = document.createElement("canvas");
@@ -2343,6 +2353,11 @@ const options = {
}, "color_blind": {
default: false, name: `Color blind mode`, help: `Removes mechanics about colors.`, restart: true,
},
+ // Could not get the sharing to work without loading androidx and all the modern android things so for now i'll just disable sharing in the android app
+ "record": !window.location.search.includes('isInWebView=true') && {
+ default: false, name: `Record games`, help: `Get a video at the end of the run.`, restart: true,
+
+ },
};
async function openSettingsPanel() {
@@ -2350,16 +2365,18 @@ async function openSettingsPanel() {
const optionsList = [];
for (const key in options) {
- optionsList.push({
- checked: isSettingOn(key) ? 1 : 0, max: 1, text: options[key].name, help: options[key].help, value: () => {
- toggleSetting(key)
- if (options[key].restart) {
- restart()
- } else {
- openSettingsPanel();
- }
- },
- });
+ if (options[key])
+ optionsList.push({
+ checked: isSettingOn(key) ? 1 : 0,
+ max: 1, text: options[key].name, help: options[key].help, value: () => {
+ toggleSetting(key)
+ if (options[key].restart) {
+ restart()
+ } else {
+ openSettingsPanel();
+ }
+ },
+ });
}
const cb = await asyncAlert({
@@ -2618,6 +2635,185 @@ function levelIconHTML(level, title) {
upgrades.forEach(u => u.icon = levelIconHTML(perkIconsLevels[u.id], u.name))
+let mediaRecorder, captureStream, recordCanvas, recordCanvasCtx, levelGif, gifCanvas, gifCtx
+
+
+function recordOneFrame() {
+ if (!isSettingOn('record')) {
+ return
+ }
+ if (!running) return;
+ drawMainCanvasOnSmallCanvas()
+ // Start recording after you hit something
+ if(levelSpawnedCoins && levelGif) {
+ recordGifFrame()
+ }
+ if (captureStream.requestFrame) {
+ captureStream.requestFrame()
+ } else {
+ captureStream.getVideoTracks()[0].requestFrame()
+ }
+
+
+}
+
+
+function drawMainCanvasOnSmallCanvas() {
+ recordCanvasCtx?.drawImage(canvas, offsetXRoundedDown, 0, gameZoneWidthRoundedUp, gameZoneHeight, 0, 0, recordCanvas.width, recordCanvas.height)
+ recordCanvasCtx.fillStyle = currentLevelInfo()?.black_puck ? '#000' : '#FFF'
+ recordCanvasCtx.textBaseline = "top";
+ recordCanvasCtx.font = "12px monospace";
+ recordCanvasCtx.textAlign = "right";
+ recordCanvasCtx.fillText(score.toString(), recordCanvas.width - 12, 12)
+ recordCanvasCtx.textAlign = "left";
+ recordCanvasCtx.fillText((currentLevel + 1) + '/' + max_levels(), 12, 12)
+}
+
+let nthFrame = 0, gifFrameReduction = 2
+function recordGifFrame(){
+ gifCtx.globalCompositeOperation = 'screen'
+ gifCtx.globalAlpha = 1 / gifFrameReduction
+ gifCtx?.drawImage(canvas, offsetXRoundedDown, 0, gameZoneWidthRoundedUp, gameZoneHeight, 0, 0, gifCanvas.width, gifCanvas.height)
+ nthFrame++
+ if (nthFrame === gifFrameReduction) {
+ levelGif.addFrame(gifCtx, {delay: Math.round(gifFrameReduction * 1000 / 60), copy: true});
+ gifCtx.globalCompositeOperation = 'source-over'
+ gifCtx.fillStyle = 'black'
+ gifCtx.fillRect(0, 0, gifCanvas.width, gifCanvas.height)
+ nthFrame=0
+ }
+}
+
+function startRecordingGame() {
+ if (!isSettingOn('record')) {
+ return
+ }
+ if (!recordCanvas) {
+ // Smaller canvas with less details
+ recordCanvas = document.createElement("canvas")
+ recordCanvasCtx = recordCanvas.getContext("2d", {antialias: false, alpha: false})
+
+ gifCanvas = document.createElement("canvas")
+ gifCtx = gifCanvas.getContext("2d", {antialias: false, alpha: false})
+ }
+
+ let scale = 1
+ while (Math.max(gameZoneWidthRoundedUp, gameZoneHeight) * scale > 400 * 2) {
+ scale = scale / 2
+ }
+ console.log('Recording at scale ' + scale)
+ recordCanvas.width = gameZoneWidthRoundedUp * scale
+ recordCanvas.height = gameZoneHeight * scale
+ gifCanvas.width = Math.floor(gameZoneWidthRoundedUp * scale / 2)
+ gifCanvas.height = Math.floor(gameZoneHeight * scale / 2)
+
+ // if(isSettingOn('basic')){
+ levelGif = new GIF({
+ workers: 2,
+ quality: 10,
+ repeat: 0,
+ background: currentLevelInfo()?.color || '#000',
+ width: gifCanvas.width,
+ height: gifCanvas.height,
+ dither: false,
+ });
+ // }else{
+ // levelGif=null
+ // }
+
+ // drawMainCanvasOnSmallCanvas()
+ const recordedChunks = [];
+ captureStream = captureStream || recordCanvas.captureStream(0);
+ const instance = new MediaRecorder(captureStream);
+ mediaRecorder = instance
+ instance.start();
+ mediaRecorder.pause()
+ instance.ondataavailable = function (event) {
+ recordedChunks.push(event.data);
+ }
+
+ instance.onstop = async function () {
+ let targetDiv = document.getElementById("level-recording-container")
+ if (!targetDiv) return
+ const video = document.createElement("video")
+ video.autoplay = true
+ video.controls = false
+ video.disablepictureinpicture = true
+ video.disableremoteplayback = true
+ video.width = recordCanvas.width
+ video.height = recordCanvas.height
+ targetDiv.style.width = recordCanvas.width + 'px'
+ targetDiv.style.height = recordCanvas.height + 'px'
+ video.loop = true
+ video.muted = true
+ video.playsinline = true
+ let blob = new Blob(recordedChunks, {type: "video/webm"});
+ video.src = URL.createObjectURL(blob);
+
+ const a = document.createElement("a")
+ a.download = captureFileName('webm')
+ a.target = "_blank"
+ a.href = video.src
+ a.textContent = `Download video (${(blob.size / 1000000).toFixed(2)}MB)`
+ targetDiv.appendChild(video)
+ targetDiv.appendChild(a)
+
+ }
+
+ levelGif?.on('finished', function (blob) {
+ let targetDiv = document.getElementById("level-recording-container")
+ const url = URL.createObjectURL(blob)
+ const img = document.createElement("img")
+ img.src = url
+ targetDiv?.appendChild(img)
+
+ const giflink = document.createElement("a")
+ giflink.textContent = `Download GIF (${(blob.size / 1000000).toFixed(2)}MB)`
+ giflink.href = url
+ giflink.download = captureFileName('gif')
+ targetDiv?.appendChild(giflink)
+ })
+
+
+}
+
+function pauseRecording() {
+ if (!isSettingOn('record')) {
+ return
+ }
+ if (mediaRecorder?.state === 'recording') {
+ mediaRecorder?.pause()
+ }
+}
+
+function resumeRecording() {
+ if (!isSettingOn('record')) {
+ return
+ }
+ if (mediaRecorder?.state === 'paused') {
+ mediaRecorder.resume()
+ }
+
+}
+
+function stopRecording() {
+
+ if (!isSettingOn('record')) {
+ return
+ }
+ if (!mediaRecorder) return;
+
+ mediaRecorder?.stop()
+ levelGif?.render()
+ mediaRecorder = null
+ levelGif = null
+}
+
+function captureFileName(ext) {
+ return "breakout-71-capture-" + new Date().toISOString().replace(/[^0-9\-]+/gi, '-') + '.' + ext
+}
+
+
fitSize()
restart()
tick();
\ No newline at end of file
diff --git a/app/src/main/assets/gif.js b/app/src/main/assets/gif.js
new file mode 100644
index 0000000..2e4d204
--- /dev/null
+++ b/app/src/main/assets/gif.js
@@ -0,0 +1,3 @@
+// gif.js 0.2.0 - https://github.com/jnordberg/gif.js
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.GIF=f()}})(function(){var define,module,exports;return function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o0&&this._events[type].length>m){this._events[type].warned=true;console.error("(node) warning: possible EventEmitter memory "+"leak detected. %d listeners added. "+"Use emitter.setMaxListeners() to increase limit.",this._events[type].length);if(typeof console.trace==="function"){console.trace()}}}return this};EventEmitter.prototype.on=EventEmitter.prototype.addListener;EventEmitter.prototype.once=function(type,listener){if(!isFunction(listener))throw TypeError("listener must be a function");var fired=false;function g(){this.removeListener(type,g);if(!fired){fired=true;listener.apply(this,arguments)}}g.listener=listener;this.on(type,g);return this};EventEmitter.prototype.removeListener=function(type,listener){var list,position,length,i;if(!isFunction(listener))throw TypeError("listener must be a function");if(!this._events||!this._events[type])return this;list=this._events[type];length=list.length;position=-1;if(list===listener||isFunction(list.listener)&&list.listener===listener){delete this._events[type];if(this._events.removeListener)this.emit("removeListener",type,listener)}else if(isObject(list)){for(i=length;i-- >0;){if(list[i]===listener||list[i].listener&&list[i].listener===listener){position=i;break}}if(position<0)return this;if(list.length===1){list.length=0;delete this._events[type]}else{list.splice(position,1)}if(this._events.removeListener)this.emit("removeListener",type,listener)}return this};EventEmitter.prototype.removeAllListeners=function(type){var key,listeners;if(!this._events)return this;if(!this._events.removeListener){if(arguments.length===0)this._events={};else if(this._events[type])delete this._events[type];return this}if(arguments.length===0){for(key in this._events){if(key==="removeListener")continue;this.removeAllListeners(key)}this.removeAllListeners("removeListener");this._events={};return this}listeners=this._events[type];if(isFunction(listeners)){this.removeListener(type,listeners)}else if(listeners){while(listeners.length)this.removeListener(type,listeners[listeners.length-1])}delete this._events[type];return this};EventEmitter.prototype.listeners=function(type){var ret;if(!this._events||!this._events[type])ret=[];else if(isFunction(this._events[type]))ret=[this._events[type]];else ret=this._events[type].slice();return ret};EventEmitter.prototype.listenerCount=function(type){if(this._events){var evlistener=this._events[type];if(isFunction(evlistener))return 1;else if(evlistener)return evlistener.length}return 0};EventEmitter.listenerCount=function(emitter,type){return emitter.listenerCount(type)};function isFunction(arg){return typeof arg==="function"}function isNumber(arg){return typeof arg==="number"}function isObject(arg){return typeof arg==="object"&&arg!==null}function isUndefined(arg){return arg===void 0}},{}],2:[function(require,module,exports){var UA,browser,mode,platform,ua;ua=navigator.userAgent.toLowerCase();platform=navigator.platform.toLowerCase();UA=ua.match(/(opera|ie|firefox|chrome|version)[\s\/:]([\w\d\.]+)?.*?(safari|version[\s\/:]([\w\d\.]+)|$)/)||[null,"unknown",0];mode=UA[1]==="ie"&&document.documentMode;browser={name:UA[1]==="version"?UA[3]:UA[1],version:mode||parseFloat(UA[1]==="opera"&&UA[4]?UA[4]:UA[2]),platform:{name:ua.match(/ip(?:ad|od|hone)/)?"ios":(ua.match(/(?:webos|android)/)||platform.match(/mac|win|linux/)||["other"])[0]}};browser[browser.name]=true;browser[browser.name+parseInt(browser.version,10)]=true;browser.platform[browser.platform.name]=true;module.exports=browser},{}],3:[function(require,module,exports){var EventEmitter,GIF,browser,extend=function(child,parent){for(var key in parent){if(hasProp.call(parent,key))child[key]=parent[key]}function ctor(){this.constructor=child}ctor.prototype=parent.prototype;child.prototype=new ctor;child.__super__=parent.prototype;return child},hasProp={}.hasOwnProperty,indexOf=[].indexOf||function(item){for(var i=0,l=this.length;iref;i=0<=ref?++j:--j){results.push(null)}return results}.call(this);numWorkers=this.spawnWorkers();if(this.options.globalPalette===true){this.renderNextFrame()}else{for(i=j=0,ref=numWorkers;0<=ref?jref;i=0<=ref?++j:--j){this.renderNextFrame()}}this.emit("start");return this.emit("progress",0)};GIF.prototype.abort=function(){var worker;while(true){worker=this.activeWorkers.shift();if(worker==null){break}this.log("killing active worker");worker.terminate()}this.running=false;return this.emit("abort")};GIF.prototype.spawnWorkers=function(){var j,numWorkers,ref,results;numWorkers=Math.min(this.options.workers,this.frames.length);(function(){results=[];for(var j=ref=this.freeWorkers.length;ref<=numWorkers?jnumWorkers;ref<=numWorkers?j++:j--){results.push(j)}return results}).apply(this).forEach(function(_this){return function(i){var worker;_this.log("spawning worker "+i);worker=new Worker(_this.options.workerScript);worker.onmessage=function(event){_this.activeWorkers.splice(_this.activeWorkers.indexOf(worker),1);_this.freeWorkers.push(worker);return _this.frameFinished(event.data)};return _this.freeWorkers.push(worker)}}(this));return numWorkers};GIF.prototype.frameFinished=function(frame){var i,j,ref;this.log("frame "+frame.index+" finished - "+this.activeWorkers.length+" active");this.finishedFrames++;this.emit("progress",this.finishedFrames/this.frames.length);this.imageParts[frame.index]=frame;if(this.options.globalPalette===true){this.options.globalPalette=frame.globalPalette;this.log("global palette analyzed");if(this.frames.length>2){for(i=j=1,ref=this.freeWorkers.length;1<=ref?jref;i=1<=ref?++j:--j){this.renderNextFrame()}}}if(indexOf.call(this.imageParts,null)>=0){return this.renderNextFrame()}else{return this.finishRendering()}};GIF.prototype.finishRendering=function(){var data,frame,i,image,j,k,l,len,len1,len2,len3,offset,page,ref,ref1,ref2;len=0;ref=this.imageParts;for(j=0,len1=ref.length;j=this.frames.length){return}frame=this.frames[this.nextFrame++];worker=this.freeWorkers.shift();task=this.getTask(frame);this.log("starting frame "+(task.index+1)+" of "+this.frames.length);this.activeWorkers.push(worker);return worker.postMessage(task)};GIF.prototype.getContextData=function(ctx){return ctx.getImageData(0,0,this.options.width,this.options.height).data};GIF.prototype.getImageData=function(image){var ctx;if(this._canvas==null){this._canvas=document.createElement("canvas");this._canvas.width=this.options.width;this._canvas.height=this.options.height}ctx=this._canvas.getContext("2d");ctx.setFill=this.options.background;ctx.fillRect(0,0,this.options.width,this.options.height);ctx.drawImage(image,0,0);return this.getContextData(ctx)};GIF.prototype.getTask=function(frame){var index,task;index=this.frames.indexOf(frame);task={index:index,last:index===this.frames.length-1,delay:frame.delay,transparent:frame.transparent,width:this.options.width,height:this.options.height,quality:this.options.quality,dither:this.options.dither,globalPalette:this.options.globalPalette,repeat:this.options.repeat,canTransfer:browser.name==="chrome"};if(frame.data!=null){task.data=frame.data}else if(frame.context!=null){task.data=this.getContextData(frame.context)}else if(frame.image!=null){task.data=this.getImageData(frame.image)}else{throw new Error("Invalid frame")}return task};GIF.prototype.log=function(){var args;args=1<=arguments.length?slice.call(arguments,0):[];if(!this.options.debug){return}return console.log.apply(console,args)};return GIF}(EventEmitter);module.exports=GIF},{"./browser.coffee":2,events:1}]},{},[3])(3)});
+//# sourceMappingURL=gif.js.map
diff --git a/app/src/main/assets/gif.worker.js b/app/src/main/assets/gif.worker.js
new file mode 100644
index 0000000..269624e
--- /dev/null
+++ b/app/src/main/assets/gif.worker.js
@@ -0,0 +1,3 @@
+// gif.worker.js 0.2.0 - https://github.com/jnordberg/gif.js
+(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o=ByteArray.pageSize)this.newPage();this.pages[this.page][this.cursor++]=val};ByteArray.prototype.writeUTFBytes=function(string){for(var l=string.length,i=0;i=0)this.dispose=disposalCode};GIFEncoder.prototype.setRepeat=function(repeat){this.repeat=repeat};GIFEncoder.prototype.setTransparent=function(color){this.transparent=color};GIFEncoder.prototype.addFrame=function(imageData){this.image=imageData;this.colorTab=this.globalPalette&&this.globalPalette.slice?this.globalPalette:null;this.getImagePixels();this.analyzePixels();if(this.globalPalette===true)this.globalPalette=this.colorTab;if(this.firstFrame){this.writeLSD();this.writePalette();if(this.repeat>=0){this.writeNetscapeExt()}}this.writeGraphicCtrlExt();this.writeImageDesc();if(!this.firstFrame&&!this.globalPalette)this.writePalette();this.writePixels();this.firstFrame=false};GIFEncoder.prototype.finish=function(){this.out.writeByte(59)};GIFEncoder.prototype.setQuality=function(quality){if(quality<1)quality=1;this.sample=quality};GIFEncoder.prototype.setDither=function(dither){if(dither===true)dither="FloydSteinberg";this.dither=dither};GIFEncoder.prototype.setGlobalPalette=function(palette){this.globalPalette=palette};GIFEncoder.prototype.getGlobalPalette=function(){return this.globalPalette&&this.globalPalette.slice&&this.globalPalette.slice(0)||this.globalPalette};GIFEncoder.prototype.writeHeader=function(){this.out.writeUTFBytes("GIF89a")};GIFEncoder.prototype.analyzePixels=function(){if(!this.colorTab){this.neuQuant=new NeuQuant(this.pixels,this.sample);this.neuQuant.buildColormap();this.colorTab=this.neuQuant.getColormap()}if(this.dither){this.ditherPixels(this.dither.replace("-serpentine",""),this.dither.match(/-serpentine/)!==null)}else{this.indexPixels()}this.pixels=null;this.colorDepth=8;this.palSize=7;if(this.transparent!==null){this.transIndex=this.findClosest(this.transparent,true)}};GIFEncoder.prototype.indexPixels=function(imgq){var nPix=this.pixels.length/3;this.indexedPixels=new Uint8Array(nPix);var k=0;for(var j=0;j=0&&x1+x=0&&y1+y>16,(c&65280)>>8,c&255,used)};GIFEncoder.prototype.findClosestRGB=function(r,g,b,used){if(this.colorTab===null)return-1;if(this.neuQuant&&!used){return this.neuQuant.lookupRGB(r,g,b)}var c=b|g<<8|r<<16;var minpos=0;var dmin=256*256*256;var len=this.colorTab.length;for(var i=0,index=0;i=0){disp=dispose&7}disp<<=2;this.out.writeByte(0|disp|0|transp);this.writeShort(this.delay);this.out.writeByte(this.transIndex);this.out.writeByte(0)};GIFEncoder.prototype.writeImageDesc=function(){this.out.writeByte(44);this.writeShort(0);this.writeShort(0);this.writeShort(this.width);this.writeShort(this.height);if(this.firstFrame||this.globalPalette){this.out.writeByte(0)}else{this.out.writeByte(128|0|0|0|this.palSize)}};GIFEncoder.prototype.writeLSD=function(){this.writeShort(this.width);this.writeShort(this.height);this.out.writeByte(128|112|0|this.palSize);this.out.writeByte(0);this.out.writeByte(0)};GIFEncoder.prototype.writeNetscapeExt=function(){this.out.writeByte(33);this.out.writeByte(255);this.out.writeByte(11);this.out.writeUTFBytes("NETSCAPE2.0");this.out.writeByte(3);this.out.writeByte(1);this.writeShort(this.repeat);this.out.writeByte(0)};GIFEncoder.prototype.writePalette=function(){this.out.writeBytes(this.colorTab);var n=3*256-this.colorTab.length;for(var i=0;i>8&255)};GIFEncoder.prototype.writePixels=function(){var enc=new LZWEncoder(this.width,this.height,this.indexedPixels,this.colorDepth);enc.encode(this.out)};GIFEncoder.prototype.stream=function(){return this.out};module.exports=GIFEncoder},{"./LZWEncoder.js":2,"./TypedNeuQuant.js":3}],2:[function(require,module,exports){var EOF=-1;var BITS=12;var HSIZE=5003;var masks=[0,1,3,7,15,31,63,127,255,511,1023,2047,4095,8191,16383,32767,65535];function LZWEncoder(width,height,pixels,colorDepth){var initCodeSize=Math.max(2,colorDepth);var accum=new Uint8Array(256);var htab=new Int32Array(HSIZE);var codetab=new Int32Array(HSIZE);var cur_accum,cur_bits=0;var a_count;var free_ent=0;var maxcode;var clear_flg=false;var g_init_bits,ClearCode,EOFCode;function char_out(c,outs){accum[a_count++]=c;if(a_count>=254)flush_char(outs)}function cl_block(outs){cl_hash(HSIZE);free_ent=ClearCode+2;clear_flg=true;output(ClearCode,outs)}function cl_hash(hsize){for(var i=0;i=0){disp=hsize_reg-i;if(i===0)disp=1;do{if((i-=disp)<0)i+=hsize_reg;if(htab[i]===fcode){ent=codetab[i];continue outer_loop}}while(htab[i]>=0)}output(ent,outs);ent=c;if(free_ent<1<0){outs.writeByte(a_count);outs.writeBytes(accum,0,a_count);a_count=0}}function MAXCODE(n_bits){return(1<0)cur_accum|=code<=8){char_out(cur_accum&255,outs);cur_accum>>=8;cur_bits-=8}if(free_ent>maxcode||clear_flg){if(clear_flg){maxcode=MAXCODE(n_bits=g_init_bits);clear_flg=false}else{++n_bits;if(n_bits==BITS)maxcode=1<0){char_out(cur_accum&255,outs);cur_accum>>=8;cur_bits-=8}flush_char(outs)}}this.encode=encode}module.exports=LZWEncoder},{}],3:[function(require,module,exports){var ncycles=100;var netsize=256;var maxnetpos=netsize-1;var netbiasshift=4;var intbiasshift=16;var intbias=1<>betashift;var betagamma=intbias<>3;var radiusbiasshift=6;var radiusbias=1<>3);var i,v;for(i=0;i>=netbiasshift;network[i][1]>>=netbiasshift;network[i][2]>>=netbiasshift;network[i][3]=i}}function altersingle(alpha,i,b,g,r){network[i][0]-=alpha*(network[i][0]-b)/initalpha;network[i][1]-=alpha*(network[i][1]-g)/initalpha;network[i][2]-=alpha*(network[i][2]-r)/initalpha}function alterneigh(radius,i,b,g,r){var lo=Math.abs(i-radius);var hi=Math.min(i+radius,netsize);var j=i+1;var k=i-1;var m=1;var p,a;while(jlo){a=radpower[m++];if(jlo){p=network[k--];p[0]-=a*(p[0]-b)/alpharadbias;p[1]-=a*(p[1]-g)/alpharadbias;p[2]-=a*(p[2]-r)/alpharadbias}}}function contest(b,g,r){var bestd=~(1<<31);var bestbiasd=bestd;var bestpos=-1;var bestbiaspos=bestpos;var i,n,dist,biasdist,betafreq;for(i=0;i>intbiasshift-netbiasshift);if(biasdist>betashift;freq[i]-=betafreq;bias[i]+=betafreq<>1;for(j=previouscol+1;j>1;for(j=previouscol+1;j<256;j++)netindex[j]=maxnetpos}function inxsearch(b,g,r){var a,p,dist;var bestd=1e3;var best=-1;var i=netindex[g];var j=i-1;while(i=0){if(i=bestd)i=netsize;else{i++;if(dist<0)dist=-dist;a=p[0]-b;if(a<0)a=-a;dist+=a;if(dist=0){p=network[j];dist=g-p[1];if(dist>=bestd)j=-1;else{j--;if(dist<0)dist=-dist;a=p[0]-b;if(a<0)a=-a;dist+=a;if(dist>radiusbiasshift;if(rad<=1)rad=0;for(i=0;i=lengthcount)pix-=lengthcount;i++;if(delta===0)delta=1;if(i%delta===0){alpha-=alpha/alphadec;radius-=radius/radiusdec;rad=radius>>radiusbiasshift;if(rad<=1)rad=0;for(j=0;j
Breakout 71
-
+
-
-
-
+
+
+
+