├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── src ├── checkers.svg ├── favicon.png ├── giphy-arts-logo.svg ├── giphy-logo.svg ├── index.html ├── marks.js ├── next.svg ├── og-image.png ├── pause.svg ├── play.svg ├── sketch.js ├── style.css ├── timeline.js ├── ui.js └── vendor │ ├── gif.js │ ├── gif.worker.js │ ├── p5.js │ └── p5.min.js └── todo-20180312.txt /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sketch Machine 2 | 3 | The Sketch Machine (www.sketchmachine.net) wants you to draw; it's waiting for you. The Sketch Machine is a quick and simple way to create short, looping animations and to turn them into GIFs to share through the web. 4 | 5 | Sketch Machine was created by Casey REAS (http://caesuras.net/) in 2018, with help from GIPHY. It was created within the tradition of “direct animation” works by Len Lye and Stan Brakhage and exploratory drawing software like TURUX. 6 | 7 | * [Sketch Machine Notes](https://github.com/REAS/sketchmachine/wiki/Sketch-Machine-Notes) 8 | * [Drawing/Animation/Coding Systems](https://github.com/REAS/sketchmachine/wiki/Drawing,-Animation,-Coding-Systems-(DACS)) 9 | * [Weird Drawing Software](https://github.com/REAS/sketchmachine/wiki/Weird-Drawing-Software) 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sketchmachine", 3 | "version": "1.0.0", 4 | "description": "", 5 | "now": { 6 | "alias": [ 7 | "sketchmachine.net", 8 | "www.sketchmachine.net" 9 | ] 10 | }, 11 | "scripts": { 12 | "start": "serve src", 13 | "dev": "browser-sync start --server 'src' --files 'src' --no-notify" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/REAS/sketchmachine.git" 18 | }, 19 | "author": "Casey REAS", 20 | "license": "GPL-2.0", 21 | "bugs": { 22 | "url": "https://github.com/REAS/sketchmachine/issues" 23 | }, 24 | "homepage": "https://github.com/REAS/sketchmachine#readme", 25 | "devDependencies": { 26 | "browser-sync": "^2.18.13" 27 | }, 28 | "dependencies": { 29 | "serve": "latest" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/checkers.svg: -------------------------------------------------------------------------------- 1 | checkers -------------------------------------------------------------------------------- /src/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/REAS/sketchmachine/54769a949ed4a4859e3e95e9343c4324865275e1/src/favicon.png -------------------------------------------------------------------------------- /src/giphy-arts-logo.svg: -------------------------------------------------------------------------------- 1 | Asset 1 -------------------------------------------------------------------------------- /src/giphy-logo.svg: -------------------------------------------------------------------------------- 1 | Asset 1 -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Sketch Machine 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 | 17 |
18 |
19 | 20 | 21 | 22 |
23 |
24 |
25 |
26 | 27 | 28 |
29 |
30 | 31 |
32 |
33 |
34 |
35 |
36 |
37 |

Drawing Tools

38 |
39 | 40 | 45 | 46 | 47 |
48 |
49 | 50 | 55 | 56 | 57 |
58 |
59 | 60 | 65 | 66 | 67 |
68 |
69 | 70 | 75 | 76 | 77 |
78 |
79 | 80 | 85 | 86 | 87 |
88 |
89 |
90 |

Modifiers

91 |
92 | 93 | Jitter 94 | 95 |
96 |
97 |
98 |
99 | 100 | 101 |
102 |
103 |
104 |
105 | 106 | Sluggish 107 | 108 |
109 |
110 |
111 |

Options

112 |
113 | 114 | 115 | 116 |
117 |
118 |
119 | Speed 120 | 121 |
122 |
123 |
124 |
125 | 126 | 127 |
128 |
129 |
130 |
131 |
132 | Playback 133 | 134 | 135 | 136 |
137 |
138 |
139 |
140 |

Export

141 |
142 |
143 |
144 |
145 |
0%
146 |
147 | 148 |
149 | 152 | 155 |
156 |
157 |
158 |
159 | 160 |
161 |
162 |
163 |
164 |
165 |
166 | 197 |
198 |
199 |
200 |
201 |
202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | -------------------------------------------------------------------------------- /src/marks.js: -------------------------------------------------------------------------------- 1 | // POINTS 2 | 3 | let pastPoints = []; 4 | for (let i = 0; i < markers.length; i += 1) { 5 | pastPoints.push({ 6 | lastPoint: { 7 | color: undefined, 8 | thickness: undefined, 9 | x: undefined, 10 | y: undefined, 11 | }, 12 | lastPointFrames: [] 13 | }) 14 | } 15 | 16 | /* 17 | function mark1(sketch, i, color, thickness) { 18 | 19 | thickness /= 4; 20 | 21 | // if (smoothing) { 22 | // mx += (targetX - mx) * easing; 23 | // my += (targetY - my) * easing; 24 | // } 25 | 26 | if (pmx !== mx || pmy !== my) { 27 | markerFrames[i].strokeCap(sketch.ROUND); 28 | markerFrames[i].noFill(); 29 | markerFrames[i].stroke(color); 30 | if (speedSize) { 31 | let varThick = sketch.map(thickness, 1, 100, 0.25, 3.0); 32 | let diameter = sketch.dist(pmx, pmy, mx, my) * varThick; 33 | markerFrames[i].strokeWeight(diameter); 34 | } else { 35 | markerFrames[i].strokeWeight(thickness + 1); 36 | } 37 | 38 | let x1 = mx + rx; 39 | let y1 = my + ry; 40 | markerFrames[i].point(x1, y1); 41 | } 42 | } 43 | */ 44 | 45 | function mark1(sketch, i, color, thickness) { 46 | 47 | thickness /= 4; 48 | 49 | /* 50 | if (smoothing) { 51 | mx += (targetX - mx) * easing; 52 | my += (targetY - my) * easing; 53 | } 54 | */ 55 | 56 | markerFrames[i].strokeCap(sketch.ROUND); 57 | markerFrames[i].stroke(color); 58 | 59 | if (speedSize) { 60 | let varThick = sketch.map(thickness, 1, 100, 0.25, 2.0); 61 | let diameter = sketch.dist(pmx, pmy, mx, my) * varThick; 62 | markerFrames[i].strokeWeight(diameter); 63 | } else { 64 | markerFrames[i].strokeWeight(thickness); 65 | } 66 | 67 | let x = mx + rx; 68 | let y = my + ry; 69 | 70 | let lastPoint = pastPoints[i].lastPoint; 71 | let lastPointFrames = pastPoints[i].lastPointFrames; 72 | 73 | let pointChanged = lastPoint.color !== color || 74 | lastPoint.thickness !== thickness || 75 | lastPoint.x !== x || 76 | lastPoint.y !== y; 77 | 78 | // markerFrames[i].drawingContext.globalCompositeOperation = 'copy'; 79 | 80 | if (pointChanged) { 81 | markerFrames[i].point(x, y); 82 | pastPoints[i].lastPointFrames = [currentFrame] 83 | } else if (lastPointFrames.includes(currentFrame) === false) { 84 | lastPointFrames.push(currentFrame); 85 | markerFrames[i].point(x, y); 86 | } 87 | 88 | // markerFrames[i].drawingContext.globalCompositeOperation = 'source-over'; 89 | 90 | pastPoints[i].lastPoint = { 91 | color: color, 92 | thickness: thickness, 93 | x: x, 94 | y: y, 95 | } 96 | } 97 | 98 | 99 | // LINES 100 | 101 | let pastLines = []; 102 | for (let i = 0; i < markers.length; i += 1) { 103 | pastLines.push({ 104 | lastLine: { 105 | color: undefined, 106 | thickness: undefined, 107 | x1: undefined, 108 | y1: undefined, 109 | x2: undefined, 110 | y2: undefined 111 | }, 112 | lastLineFrames: [] 113 | }) 114 | } 115 | 116 | function mark2(sketch, i, color, thickness) { 117 | 118 | /* 119 | if (smoothing) { 120 | mx += (targetX - mx) * easing; 121 | my += (targetY - my) * easing; 122 | } 123 | */ 124 | 125 | markerFrames[i].strokeCap(sketch.ROUND); 126 | markerFrames[i].strokeJoin(sketch.ROUND); 127 | markerFrames[i].stroke(color); 128 | markerFrames[i].noFill(); 129 | if (speedSize) { 130 | let varThick = sketch.map(thickness, 1, 100, 0.25, 2.0); 131 | let diameter = sketch.dist(pmx, pmy, mx, my) * varThick; 132 | markerFrames[i].strokeWeight(diameter); 133 | } else { 134 | markerFrames[i].strokeWeight(thickness); 135 | } 136 | 137 | let x0 = ppmx + pprx; 138 | let y0 = ppmy + ppry; 139 | let x1 = pmx + prx; 140 | let y1 = pmy + pry; 141 | let x2 = mx + rx; 142 | let y2 = my + ry; 143 | 144 | let lastLine = pastLines[i].lastLine; 145 | let lastLineFrames = pastLines[i].lastLineFrames; 146 | 147 | let lineChanged = lastLine.color !== color || 148 | lastLine.thickness !== thickness || 149 | lastLine.x1 !== x1 || 150 | lastLine.y1 !== y1 || 151 | lastLine.x2 !== x2 || 152 | lastLine.y2 !== y2; 153 | 154 | let points 155 | 156 | if (lineChanged) { 157 | points = curvePoints(x0, y0, x1, y1, x2, y2); 158 | pastLines[i].lastLineFrames = [currentFrame]; 159 | } else if (lastLineFrames.includes(currentFrame) === false) { 160 | lastLineFrames.push(currentFrame); 161 | points = curvePoints(x0, y0, x1, y1, x2, y2); 162 | } 163 | 164 | if (points) { 165 | if (points.length === 2 && points[0][0] === points[1][0] && points[0][1] === points[1][1]) { 166 | markerFrames[i].point(...points[0]) 167 | } else if (points.length === 0) { 168 | markerFrames[i].point(x2, y2); 169 | } else { 170 | markerFrames[i].beginShape(); 171 | points.forEach((p) => { markerFrames[i].vertex(...p); }); 172 | markerFrames[i].endShape(); 173 | } 174 | } 175 | 176 | pastLines[i].lastLine = { 177 | color: color, 178 | thickness: thickness, 179 | x1: x1, 180 | y1: y1, 181 | x2: x2, 182 | y2: y2, 183 | } 184 | } 185 | 186 | // QUADS 187 | 188 | function mark3(sketch, i, color, thickness) { 189 | /* 190 | if (smoothing) { 191 | mx += (targetX - mx) * easing; 192 | my += (targetY - my) * easing; 193 | } 194 | */ 195 | 196 | if (pmx !== mx || pmy !== my) { 197 | markerFrames[i].strokeCap(sketch.SQUARE); 198 | markerFrames[i].noFill(); 199 | markerFrames[i].stroke(color); 200 | if (speedSize) { 201 | let varThick = sketch.map(thickness, 1, 100, 0.25, 3.0); 202 | let diameter = sketch.dist(pmx, pmy, mx, my) * varThick; 203 | markerFrames[i].strokeWeight(diameter); 204 | } else { 205 | markerFrames[i].strokeWeight(thickness + 1); 206 | } 207 | 208 | let x1 = pmx + prx; 209 | let y1 = pmy + pry; 210 | let x2 = mx + rx; 211 | let y2 = my + ry; 212 | markerFrames[i].line(x1, y1, x2, y2); 213 | } 214 | } 215 | 216 | 217 | // Utility functions for curves 218 | 219 | function curvePoints(x0, y0, x1, y1, x2, y2) { 220 | // Update pastPast, past, and current points. 221 | let midp1 = midp([], [x0, y0], [x1, y1]); 222 | let midp2 = midp([], [x1, y1], [x2, y2]); 223 | 224 | let flow = 1; 225 | 226 | // Make a low res (flattened) quadratic bezier curve from three control points, so that a completely new curve is generated each time a mouse coordinate gets added. 227 | // A: Midpoint of past and pastPast points. 228 | // B: pastPoint 229 | // C: Midpoint of pastPoint and current point. 230 | let dist = Math.hypot(midp2[0] - midp1[0], midp2[1] - midp1[1]); 231 | let segmentCount = 1 + Math.floor(dist / flow); 232 | let step = 1 / segmentCount; 233 | let bezierPoints = []; 234 | for (let i = 0; i <= segmentCount; i += 1) { 235 | let t = i*step; 236 | bezierPoints.push(quadBez(t, midp1, [x1, y1], midp2)); 237 | } 238 | 239 | return bezierPoints 240 | } 241 | 242 | function quadBez (t, p1, p2, p3) { 243 | return [quadBezXY(t, [p1[0], p2[0], p3[0]]), quadBezXY(t, [p1[1], p2[1], p3[1]])] 244 | } 245 | 246 | function quadBezXY(t, w) { 247 | t2 = t * t; 248 | mt = 1 - t; 249 | mt2 = mt * mt; 250 | return w[0] * mt2 + w[1] * 2 * mt * t + w[2] * t2 251 | } 252 | 253 | function midp (out, a, b) { 254 | out[0] = (a[0] + b[0]) / 2; 255 | out[1] = (a[1] + b[1]) / 2; 256 | return out 257 | } 258 | -------------------------------------------------------------------------------- /src/next.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 9 | 10 | -------------------------------------------------------------------------------- /src/og-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/REAS/sketchmachine/54769a949ed4a4859e3e95e9343c4324865275e1/src/og-image.png -------------------------------------------------------------------------------- /src/pause.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/play.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/sketch.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let frames = []; 3 | let backgroundFrame; 4 | let markerFrames = []; 5 | let compositeFrame; 6 | let exportFrame; 7 | 8 | let numFrames = 36; // 48 9 | let numMarkerFrames = 5; 10 | let firstFrame = 12; //18; 11 | let lastFrame = 24; //30; 12 | let currentFrame = firstFrame; 13 | 14 | let timelineRangeSelected = false; 15 | let timelineRangeLock = false; 16 | let arrowLock = false; 17 | 18 | let numToLeft = 0; 19 | let numToRight = 0; 20 | 21 | let jitterOn = false; 22 | 23 | let frameDim = 512; 24 | let surfaceDim = frameDim; 25 | if (window.screen.availWidth >= 1024) { 26 | surfaceDim = Math.min(1024, 512 * window.devicePixelRatio); 27 | } 28 | let resInt = parseInt(window.location.search.replace('?res=', '')); 29 | if (resInt) { surfaceDim = Math.min(1024, resInt); } 30 | let surfaceBorder = 4; 31 | let frameSurfaceRatio = frameDim / surfaceDim; 32 | 33 | let lastTime = 0; 34 | let timeStep = 500; // In milliseconds 35 | 36 | let pause = false; 37 | let startDrawing = false; 38 | let didDrawAnything = false; 39 | 40 | let mx = 0; 41 | let my = 0; 42 | let pmx = 0 43 | let pmy = 0; 44 | let ppmx = 0; 45 | let ppmy = 0; 46 | 47 | let rx = 0; 48 | let ry = 0; 49 | let prx = 0 50 | let pry = 0; 51 | let pprx = 0; 52 | let ppry = 0; 53 | 54 | let targetX = 0; 55 | let targetY = 0; 56 | 57 | let currentColor = "#FFFFFF"; 58 | let currentColorSelection = 1; 59 | 60 | const ui = document.querySelector('#ui'); 61 | const sketchContainer = document.querySelector('#sketch-container'); 62 | const animationDummy = document.querySelector('#animation-dummy'); 63 | const speedSlider = document.querySelector('#speed'); 64 | 65 | const colorSelector = document.querySelector('#color-selector'); 66 | const backgroundColorSelector = document.querySelector('#background-color-selector'); 67 | const backgroundColorButton = document.querySelector('#background-color-button'); 68 | 69 | let speedSize = false; 70 | let onionSkin = false; 71 | let smoothing = false; 72 | let easingSlider = document.querySelector("#easing-slider"); 73 | let easing = 0.0; 74 | 75 | // TIMELINE 76 | //let overFrame = new Array(numFrames).fill(false); 77 | //let overMarker = new Array(numFrames).fill(false); 78 | let onFrame = new Array(numFrames).fill(false); 79 | //let firstClick = false; 80 | //let addMode = true; // Add or remove active frames 81 | 82 | let playbackDirection = 1; 83 | 84 | const REVERSE = 0; 85 | const FORWARD = 1; 86 | const BACKANDFORTH = 2; 87 | 88 | let playbackMode = FORWARD; 89 | const fpsOptions = [20, 30, 40, 60, 80, 120, 250, 500, 1000]; 90 | 91 | // MARKERS 92 | const marker1Select = document.querySelector("#b1-select"); 93 | const marker1Tools = document.querySelector("#b1-tools"); 94 | const marker1Slider = document.querySelector("#b1-slider"); 95 | const marker1ColorButton = document.querySelector("#b1-color"); 96 | let marker1Color = 0; 97 | 98 | const marker2Select = document.querySelector("#b2-select"); 99 | const marker2Tools = document.querySelector("#b2-tools"); 100 | const marker2Slider = document.querySelector("#b2-slider"); 101 | const marker2ColorButton = document.querySelector("#b2-color"); 102 | let marker2Color = 0; 103 | 104 | const marker3Select = document.querySelector("#b3-select"); 105 | const marker3Tools = document.querySelector("#b3-tools"); 106 | const marker3Slider = document.querySelector("#b3-slider"); 107 | const marker3ColorButton = document.querySelector("#b3-color"); 108 | let marker3Color = 0; 109 | 110 | const marker4Select = document.querySelector("#b4-select"); 111 | const marker4Tools = document.querySelector("#b4-tools"); 112 | const marker4Slider = document.querySelector("#b4-slider"); 113 | const marker4ColorButton = document.querySelector("#b4-color"); 114 | let marker4Color = 0; 115 | 116 | const marker5Select = document.querySelector("#b5-select"); 117 | const marker5Tools = document.querySelector("#b5-tools"); 118 | const marker5Slider = document.querySelector("#b5-slider"); 119 | const marker5ColorButton = document.querySelector("#b5-color"); 120 | let marker5Color = 0; 121 | 122 | let backgroundColor = 0; 123 | let backgroundEnabled = true; 124 | let randomXY = document.querySelector("#randomXY"); 125 | let markers = [false, false, false, false, false]; 126 | 127 | let timelineCanvas; 128 | 129 | const animationSketch = new p5(function (sketch) { 130 | sketch.setup = function() { 131 | sketch.pixelDensity(window.devicePixelRatio); 132 | canvas = sketch.createCanvas(frameDim, frameDim); 133 | 134 | if (surfaceDim <= frameDim) { sketch.noSmooth() } 135 | 136 | /* 137 | for (let i = firstFrame; i < lastFrame; i++) { 138 | onFrame[i] = true; 139 | } 140 | */ 141 | 142 | canvas.id('animation'); 143 | 144 | animationDummy.remove(); 145 | sketchContainer.prepend(canvas.elt); 146 | canvas.background(204); 147 | 148 | marker1Color = web216[sketch.int(sketch.random(web216.length))]; //"#FFFFFF"; 149 | marker2Color = web216[sketch.int(sketch.random(web216.length))]; 150 | marker3Color = web216[sketch.int(sketch.random(web216.length))]; 151 | marker4Color = web216[sketch.int(sketch.random(web216.length))]; 152 | marker5Color = web216[sketch.int(sketch.random(web216.length))]; 153 | 154 | marker1ColorButton.style.backgroundColor = marker1Color; 155 | marker2ColorButton.style.backgroundColor = marker2Color; 156 | marker3ColorButton.style.backgroundColor = marker3Color; 157 | marker4ColorButton.style.backgroundColor = marker4Color; 158 | marker5ColorButton.style.backgroundColor = marker5Color; 159 | 160 | backgroundColor = web216[sketch.int(sketch.random(web216.length))]; //"#000000"; // 161 | backgroundColorButton.style.backgroundColor = backgroundColor; 162 | 163 | sketch.pixelDensity(1); 164 | 165 | for (let i = 0; i < numFrames; i++) { 166 | frames[i] = sketch.createGraphics(surfaceDim, surfaceDim); 167 | } 168 | 169 | for (let i = 0; i < numMarkerFrames; i++) { 170 | markerFrames[i] = sketch.createGraphics(surfaceDim, surfaceDim); 171 | } 172 | 173 | compositeFrame = sketch.createGraphics(surfaceDim, surfaceDim); 174 | exportFrame = sketch.createGraphics(surfaceDim, surfaceDim); 175 | 176 | backgroundFrame = sketch.createGraphics(surfaceDim, surfaceDim); 177 | backgroundFrame.background(backgroundColor); 178 | 179 | sketch.pixelDensity(window.devicePixelRatio); 180 | 181 | lastTime = sketch.millis(); 182 | }; 183 | 184 | sketch.draw = function () { 185 | if (startDrawing) { 186 | 187 | if (smoothing) { 188 | targetX = Math.floor((sketch.mouseX - surfaceBorder) / frameSurfaceRatio) 189 | targetY = Math.floor((sketch.mouseY - surfaceBorder) / frameSurfaceRatio) 190 | } else { 191 | mx = Math.floor((sketch.mouseX - surfaceBorder) / frameSurfaceRatio) 192 | my = Math.floor((sketch.mouseY - surfaceBorder) / frameSurfaceRatio) 193 | } 194 | } 195 | 196 | // TIMELINE 197 | timeStep = fpsOptions[parseInt(speedSlider.value)-1]; 198 | 199 | // MARKERS 200 | markers[0] = marker1Select.checked; 201 | markers[1] = marker2Select.checked; 202 | markers[2] = marker3Select.checked; 203 | markers[3] = marker4Select.checked; 204 | markers[4] = marker5Select.checked; 205 | 206 | let whichTool; 207 | 208 | let markFunctions = [ mark1, mark2, mark3 ]; 209 | 210 | let rxy = parseInt(randomXY.value); 211 | 212 | rx = 0; 213 | ry = 0; 214 | 215 | let tempEasing = parseInt(easingSlider.value); 216 | if (tempEasing > 0) { 217 | easing = sketch.map(tempEasing, 0, 100, 0.1, 0.02); 218 | //smoothing = true; 219 | } else { 220 | //smoothing = false; 221 | } 222 | 223 | //if (rxy !== 0) { 224 | if (jitterOn) { 225 | rx = sketch.random(-rxy, rxy); 226 | ry = sketch.random(-rxy, rxy); 227 | } 228 | //} 229 | 230 | if (startDrawing) { 231 | if (smoothing) { 232 | mx += (targetX - mx) * easing; 233 | my += (targetY - my) * easing; 234 | } 235 | if (markers[0]) { 236 | whichTool = parseInt(marker1Tools.value); 237 | markFunctions[whichTool - 1](sketch, 0, marker1Color, calculateThickness(marker1Slider.value)); 238 | didDrawAnything = true 239 | } 240 | if (markers[1]) { 241 | whichTool = parseInt(marker2Tools.value); 242 | markFunctions[whichTool - 1](sketch, 1, marker2Color, calculateThickness(marker2Slider.value)); 243 | didDrawAnything = true 244 | } 245 | if (markers[2]) { 246 | whichTool = parseInt(marker3Tools.value); 247 | markFunctions[whichTool - 1](sketch, 2, marker3Color, calculateThickness(marker3Slider.value)); 248 | didDrawAnything = true 249 | } 250 | if (markers[3]) { 251 | whichTool = parseInt(marker4Tools.value); 252 | markFunctions[whichTool - 1](sketch, 3, marker4Color, calculateThickness(marker4Slider.value)); 253 | didDrawAnything = true 254 | } 255 | if (markers[4]) { 256 | whichTool = parseInt(marker5Tools.value); 257 | markFunctions[whichTool - 1](sketch, 4, marker5Color, calculateThickness(marker5Slider.value)); 258 | didDrawAnything = true 259 | } 260 | } 261 | 262 | // Now, finally, draw the animation to the screen 263 | 264 | if (!pause || startDrawing) { 265 | displayFrame(sketch) 266 | } 267 | 268 | if (startDrawing) { 269 | ppmx = pmx; 270 | ppmy = pmy; 271 | pprx = prx; 272 | ppry = pry; 273 | pmx = mx; 274 | pmy = my; 275 | prx = rx; 276 | pry = ry; 277 | } 278 | 279 | if (!pause) { 280 | if (sketch.millis() > lastTime + timeStep) { 281 | writeMarkersIntoFrames(); 282 | 283 | if (playbackMode === FORWARD) { 284 | currentFrame++; // Go to the next frame 285 | if (currentFrame >= lastFrame) { 286 | currentFrame = firstFrame; 287 | } 288 | } else if (playbackMode === REVERSE) { 289 | currentFrame--; // Go to the next frame 290 | if (currentFrame < firstFrame) { 291 | currentFrame = lastFrame - 1; 292 | } 293 | } else if (playbackMode === BACKANDFORTH) { 294 | currentFrame += playbackDirection; // Go to the next frame 295 | if (currentFrame >= lastFrame - 1 || currentFrame <= firstFrame) { 296 | playbackDirection *= -1; 297 | if (currentFrame >= lastFrame - 1) { currentFrame = lastFrame - 1 } 298 | if (currentFrame <= firstFrame) { currentFrame = firstFrame } 299 | } 300 | } 301 | lastTime = sketch.millis(); 302 | } 303 | } 304 | }; 305 | 306 | // Define an abstract pointer device 307 | function pointerPressed (e) { 308 | 309 | // Cancel event if not clicking inside a sketch. 310 | if (e.target !== canvas.elt && e.target !== timelineCanvas.elt) { 311 | return 312 | } 313 | 314 | // If click in animation area 315 | if (sketch.mouseX > 0 && sketch.mouseX < sketch.width && sketch.mouseY > 0 && sketch.mouseY < sketch.height) { 316 | startDrawing = true; 317 | pastLines.forEach((line) => { line.lastLineFrames = [] }); 318 | pastPoints.forEach((point) => { point.lastPointFrames = [] }); 319 | pmx = Math.floor((sketch.mouseX - surfaceBorder) / frameSurfaceRatio); 320 | pmy = Math.floor((sketch.mouseY - surfaceBorder) / frameSurfaceRatio); 321 | ppmx = pmx; 322 | ppmy = pmy; 323 | if (smoothing) { 324 | mx = pmx; 325 | my = pmy; 326 | } 327 | } 328 | } 329 | 330 | function pointerReleased() { 331 | startDrawing = false; 332 | arrowLock = false; 333 | selectFirstFrame = false; 334 | selectLastFrame = false; 335 | timelineRangeLock = false; 336 | writeMarkersIntoFrames(); 337 | } 338 | 339 | sketch.mousePressed = (e) => { pointerPressed(e) }; 340 | sketch.mouseReleased = () => { pointerReleased() }; 341 | 342 | sketch.touchStarted = (e) => { pointerPressed(e) }; 343 | 344 | sketch.touchMoved = (e) => { 345 | 346 | if (e.touches && e.touches.length === 2) { 347 | e.preventDefault() 348 | } 349 | 350 | if (startDrawing && e.touches && e.touches.length === 1) { // Prevent pan gesture on mobile 351 | e.preventDefault(); 352 | } 353 | }; 354 | 355 | sketch.touchReleased = () => { pointerReleased() }; 356 | }); 357 | 358 | const timelineSketch = new p5(function (sketch) { 359 | sketch.setup = function () { 360 | sketch.pixelDensity(window.devicePixelRatio); 361 | timelineCanvas = sketch.createCanvas(frameDim, 110); 362 | timelineCanvas.id('timeline'); 363 | sketchContainer.append(timelineCanvas.elt); 364 | sketch.noSmooth(); 365 | } 366 | 367 | sketch.draw = function () { 368 | if (!pause || sketch.mouseIsPressed) { 369 | displayTimeline(sketch) 370 | } 371 | } 372 | 373 | sketch.mouseMoved = function (e) { 374 | if (timelineCanvas && e.target !== timelineCanvas.elt) { return } 375 | if (sketch.mouseX > 0 && sketch.mouseX < sketch.width && sketch.mouseY > 0 && sketch.mouseY < sketch.height) { 376 | displayTimeline(sketch) 377 | } 378 | } 379 | 380 | sketch.mouseDragged = function (e) { 381 | if (sketch.mouseX > 0 && sketch.mouseX < sketch.width && sketch.mouseY > 0 && sketch.mouseY < sketch.height) { 382 | displayFrame(animationSketch) 383 | } 384 | } 385 | 386 | sketch.mousePressed = function (e) { 387 | if (sketch.mouseX > 0 && sketch.mouseX < sketch.width && sketch.mouseY > 0 && sketch.mouseY < sketch.height) { 388 | displayFrame(animationSketch) 389 | } 390 | } 391 | 392 | }); 393 | 394 | function displayFrame (sketch) { 395 | canvas.drawingContext.clearRect(0, 0, sketch.width, sketch.height); 396 | if (backgroundEnabled === true) { 397 | backgroundFrame.background(backgroundColor); 398 | sketch.drawingContext.drawImage(backgroundFrame.canvas, 0, 0, frameDim, frameDim) 399 | } 400 | sketch.drawingContext.drawImage(frames[currentFrame].canvas, 0, 0, frameDim, frameDim); 401 | for (let i = numMarkerFrames-1; i >= 0; i--) { 402 | sketch.drawingContext.drawImage(markerFrames[i].canvas, 0, 0, frameDim, frameDim); 403 | } 404 | 405 | if (pause && onionSkin) { 406 | sketch.drawingContext.globalAlpha = 0.5; 407 | if (currentFrame > firstFrame) { 408 | sketch.drawingContext.drawImage(frames[currentFrame - 1].canvas, 0, 0, frameDim, frameDim); 409 | } else if (currentFrame === firstFrame) { 410 | sketch.drawingContext.drawImage(frames[lastFrame - 1].canvas, 0, 0, frameDim, frameDim); 411 | } 412 | sketch.drawingContext.globalAlpha = 1.0 413 | } 414 | } 415 | 416 | function writeMarkersIntoFrames () { 417 | // Composite each layer into one, then erase in turn 418 | for (let i = numMarkerFrames - 1; i >= 0; i--) { 419 | if (markers[i]) { 420 | compositeFrame.drawingContext.drawImage(markerFrames[i].canvas, 0, 0, surfaceDim, surfaceDim) 421 | markerFrames[i].drawingContext.clearRect(0, 0, surfaceDim, surfaceDim) 422 | } 423 | } 424 | 425 | // Write all "marker frames" composites into the selected frames 426 | for (let i = firstFrame; i < lastFrame; i++) { 427 | if (onFrame[i] || i === currentFrame) { 428 | frames[i].drawingContext.drawImage(compositeFrame.canvas, 0, 0, surfaceDim, surfaceDim) 429 | } 430 | } 431 | compositeFrame.drawingContext.clearRect(0, 0, surfaceDim, surfaceDim); 432 | } 433 | 434 | function eraseFrame() { 435 | if (window.confirm("Clear this frame?")) { 436 | frames[currentFrame].clear(); 437 | displayFrame(animationSketch) 438 | } 439 | } 440 | 441 | function eraseAllFrames() { 442 | if (window.confirm("Are you sure you want to clear everything?")) { 443 | for (let i = 0; i < numFrames; i++) { 444 | frames[i].clear(); 445 | } 446 | displayFrame(animationSketch) 447 | } 448 | } 449 | 450 | function eraseSelectedFrames() { 451 | if (window.confirm("Are you sure you want to clear the selected frames?")) { 452 | frames[currentFrame].clear(); 453 | for (let i = 0; i < numFrames; i++) { 454 | if (onFrame[i]) { 455 | frames[i].clear(); 456 | } 457 | } 458 | displayFrame(animationSketch) 459 | } 460 | } 461 | 462 | window.addEventListener('keydown', (e) => { 463 | 464 | if(e.altKey || e.shiftKey) { 465 | return 466 | } 467 | 468 | if (e.key === 'f' || e.key === 'F') { 469 | eraseFrame(); 470 | } 471 | 472 | if (e.key === 'a' || e.key === 'A') { 473 | eraseAllFrames(); 474 | } 475 | 476 | if (e.key === 's' || e.key === 'S') { 477 | eraseSelectedFrames(); 478 | } 479 | 480 | if (e.key === 'p' || e.key === 'P') { 481 | clickPlay(); 482 | } 483 | 484 | if (e.keyCode === 37) { // Left arrow 485 | if (!pause) { clickPlay() } else { 486 | clickBack() 487 | } 488 | e.preventDefault(); 489 | } 490 | if (e.keyCode === 39) { // Right arrow 491 | if (!pause) { clickPlay() } else { 492 | clickNext() 493 | } 494 | e.preventDefault(); 495 | } 496 | 497 | /* 498 | if (e.key === 'u' || e.key === 'U') { 499 | onFrame.fill(false); 500 | } 501 | if (e.key === 'a' || e.key === 'A') { 502 | onFrame.fill(true); 503 | } 504 | */ 505 | }) 506 | 507 | function calculateThickness(t) { 508 | return Math.max(1, Math.floor(parseInt(t) / frameSurfaceRatio)) 509 | } 510 | 511 | // GIF Export 512 | const exportButton = document.getElementById('export-button'); 513 | const exportOverlay = document.getElementById('export-overlay'); 514 | const exportedGIFSpinner = document.getElementById('exported-gif-spinner'); 515 | const exportedGIFImg = document.getElementById('exported-gif-img'); 516 | const exportControls = document.getElementById('export-controls'); 517 | const exportProgress = document.getElementById('export-progress'); 518 | 519 | function renderFrameGIF (gif, i) { 520 | if (backgroundEnabled === true) { 521 | exportFrame.drawingContext.drawImage(backgroundFrame.canvas, 0, 0, surfaceDim, surfaceDim); 522 | } else { 523 | exportFrame.drawingContext.clearRect(0, 0, surfaceDim, surfaceDim); 524 | } 525 | exportFrame.drawingContext.drawImage(frames[i].canvas, 0, 0, surfaceDim, surfaceDim); 526 | gif.addFrame(exportFrame.canvas, {delay: timeStep, copy: true}); 527 | } 528 | 529 | let checkInterval; 530 | let gifBlob; 531 | 532 | function exportGIF () { 533 | if (didDrawAnything === false) { 534 | alert("You haven't drawn anything to export."); 535 | return 536 | } 537 | 538 | if(!pause) { clickPlay() } 539 | 540 | exportOverlay.classList.add('active'); 541 | exportButton.classList.add('active'); 542 | document.body.classList.add('noscroll'); 543 | 544 | let isTransparent = null; 545 | if (backgroundEnabled === false) { isTransparent = 0x000000 } 546 | 547 | const gif = new GIF({ 548 | workers: 4, 549 | quality: 10, 550 | background: backgroundColor, 551 | transparent: isTransparent, 552 | workerScript: "vendor/gif.worker.js" 553 | }); 554 | 555 | switch(playbackMode) { 556 | case FORWARD: 557 | for (let i = firstFrame; i < lastFrame; i += 1) { renderFrameGIF(gif, i) } 558 | break; 559 | case REVERSE: 560 | for (let i = lastFrame - 1; i > firstFrame; i -= 1) { renderFrameGIF(gif, i) } 561 | break; 562 | case BACKANDFORTH: 563 | for (let i = firstFrame; i < lastFrame; i += 1) { renderFrameGIF(gif, i) } 564 | for (let i = lastFrame - 2; i >= firstFrame + 1; i -= 1) { renderFrameGIF(gif, i) } 565 | break; 566 | } 567 | 568 | let totalFrames = gif.frames.length; 569 | 570 | checkInterval = window.setInterval(() => { 571 | let progress = gif.finishedFrames / totalFrames; 572 | exportedGIFSpinner.style.borderRadius = ((1 - progress) * 50) + '%'; 573 | exportProgress.innerText = Math.round(progress * 100) + '%' 574 | }, 150); 575 | 576 | gif.on('finished', function(blob) { 577 | if (exportOverlay.classList.contains('active')) { 578 | window.clearInterval(checkInterval); 579 | exportedGIFImg.onload = function () { 580 | exportedGIFImg.classList.add('active'); 581 | exportControls.classList.add('active'); 582 | exportedGIFSpinner.classList.add('hidden') 583 | } 584 | gifBlob = blob; 585 | exportedGIFImg.src = URL.createObjectURL(blob); 586 | } 587 | }); 588 | 589 | gif.render() 590 | } 591 | 592 | exportOverlay.onclick = (e) => { 593 | if (exportedGIFImg.classList.contains('active') === false) { 594 | cancelOrCloseGIF() 595 | } 596 | } 597 | 598 | function cancelOrCloseGIF () { 599 | window.clearInterval(checkInterval); 600 | gifBlob = undefined; 601 | exportedGIFSpinner.removeAttribute('style'); 602 | exportProgress.innerText = '0%'; 603 | exportOverlay.classList.remove('active'); 604 | exportButton.classList.remove('active'); 605 | exportedGIFSpinner.classList.remove('hidden'); 606 | exportedGIFImg.classList.remove('active'); 607 | exportControls.classList.remove('active'); 608 | document.body.classList.remove('noscroll'); 609 | exportUploadToGiphy.classList.remove('hidden'); 610 | giphyUploadOverlay.classList.remove('active'); 611 | } 612 | 613 | const exportUploadToGiphy = document.getElementById('export-upload-to-giphy'); 614 | const giphyUploadOverlay = document.getElementById('giphy-upload-overlay'); 615 | 616 | function uploadToGiphy () { 617 | 618 | exportUploadToGiphy.classList.add('hidden'); 619 | giphyUploadOverlay.classList.add('active'); 620 | 621 | let username = 'sketchmachine'; 622 | let apiKey = 'ul0HNk8uS4G92IEFad7Y9XabW2pOBfK9'; 623 | 624 | let formData = new FormData(); 625 | formData.append('file', gifBlob, 'sketchmachine.gif'); 626 | formData.append('username', username); 627 | formData.append('api_key', apiKey); 628 | formData.append('tags', 'sketchmachine'); 629 | formData.append('source_post_url', 'https://sketchmachine.net'); 630 | 631 | fetch('https://upload.giphy.com/v1/gifs', { 632 | method: 'POST', 633 | body: formData, 634 | mode: 'cors' 635 | }).then((response) => { 636 | return response.json() 637 | }) 638 | .catch((error) => { console.error('Error:', error) }) 639 | .then((res) => { 640 | if (res.meta && res.meta.status === 200) { 641 | let id = res.data.id; 642 | let url = 'https://giphy.com/gifs/' + username + '-' + id; 643 | window.open(url); 644 | giphyUploadOverlay.classList.remove('active'); 645 | } else { 646 | console.error('Upload failed.'); 647 | exportUploadToGiphy.classList.remove('hidden'); 648 | giphyUploadOverlay.classList.remove('active'); 649 | } 650 | }); 651 | } 652 | 653 | // Leave page warning 654 | window.onbeforeunload = (e) => { 655 | if (didDrawAnything) { 656 | return true 657 | } 658 | } -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | html { 2 | touch-action: manipulation; 3 | } 4 | 5 | body { 6 | margin: 0; 7 | background-color: #CCC; 8 | user-select: none; 9 | -webkit-text-size-adjust: none; 10 | } 11 | 12 | body::after { 13 | position:absolute; width:0; height:0; overflow:hidden; z-index:-1; 14 | content:url(play.svg); 15 | } 16 | 17 | body.noscroll { 18 | height: 100%; 19 | overflow: hidden; 20 | } 21 | 22 | /* Override p5.js sizing to fit viewport. */ 23 | #animation, #animation-dummy { 24 | width: calc(100% - 8px) !important; 25 | height: auto !important; 26 | cursor: crosshair; 27 | background: url(checkers.svg); 28 | background-size: 16px; 29 | border: 4px inset; 30 | } 31 | 32 | #timeline { 33 | margin-left: 4px; 34 | max-width: 512px; 35 | width: calc(100% - 8px) !important; 36 | height: auto !important; 37 | } 38 | 39 | input, button { 40 | -webkit-tap-highlight-color: rgba(0,0,0,0); 41 | } 42 | 43 | input[type="button"], button { 44 | -webkit-appearance: none; 45 | } 46 | 47 | #color-selector { 48 | width: 100%; 49 | height: 100%; 50 | background-color: black; 51 | display: none; 52 | position: fixed; 53 | top: 0; 54 | left: 0; 55 | } 56 | 57 | #colors { 58 | font-size: 0; 59 | height: 100%; 60 | overflow-y: scroll; 61 | -webkit-overflow-scrolling: touch; 62 | } 63 | 64 | #colors button { 65 | width: 12.5%; 66 | padding: 12.5% 0 0 0; 67 | border: 0; 68 | /*outline: 0;*/ 69 | cursor: pointer; 70 | margin: 0; 71 | } 72 | 73 | #colors button:active { 74 | filter: invert(1); 75 | } 76 | 77 | #color-selector.active { 78 | display: block; 79 | } 80 | 81 | #background-color-selector.active { 82 | display: block; 83 | } 84 | 85 | #ui { 86 | padding: 21px 10px 10px 10px; 87 | box-sizing: border-box; 88 | margin: auto; 89 | display: flex; 90 | flex-direction: column; 91 | position: relative; 92 | user-select: none; 93 | } 94 | 95 | #column { 96 | max-width: 400px; 97 | /*background-color: pink;*/ 98 | } 99 | 100 | #macro-ui { 101 | margin: auto; 102 | display: flex; 103 | flex-direction: column; 104 | position: relative; 105 | } 106 | 107 | #etc { 108 | font-family: monospace; 109 | font-size: 12px; 110 | color: #666666; 111 | box-sizing: border-box; 112 | width: 100%; 113 | display: flex; 114 | flex-direction: column; 115 | position: relative; 116 | max-width: 520px; 117 | margin: auto; 118 | } 119 | 120 | footer { 121 | padding: 10px; 122 | } 123 | 124 | #etc a, #etc a:visited { 125 | color: #666666; 126 | } 127 | 128 | .current-resolution { 129 | color: #111 !important; 130 | } 131 | 132 | #fine-print, #fine-print a, #fine-print a:visited { 133 | /*color: #AAA;*/ 134 | } 135 | 136 | hr { 137 | margin-top: 10px; 138 | margin-bottom: 0px; 139 | width: 100%; 140 | border-left: none; 141 | border-right: none; 142 | } 143 | 144 | #sketch-container { 145 | position: relative; 146 | max-width: 520px; 147 | max-height: 680px; /*662*/ 148 | display: flex; 149 | flex-direction: column; 150 | } 151 | 152 | #buttons { 153 | height: 50px; 154 | width: 100%; 155 | padding-top: 4px; 156 | margin: 2px auto 0 auto; 157 | display: flex; 158 | vertical-align: middle; 159 | } 160 | 161 | #playback-buttons { 162 | margin-left: 4px; 163 | height: 50px; 164 | } 165 | 166 | #playback-buttons input { 167 | cursor: pointer; 168 | } 169 | 170 | .colorButton { 171 | width: 24px; 172 | height: 24px; 173 | color: white; 174 | padding: 2px 2px 2px 2px; 175 | display: inline-block; 176 | border-radius: 50%; 177 | border: 1px solid #FFFFFF; 178 | cursor: pointer; 179 | } 180 | 181 | .play { 182 | background: url("pause.svg") no-repeat top left; 183 | background-size: contain; 184 | width: 50px; 185 | height: 50px; 186 | display: inline-block; 187 | border-radius: 50%; 188 | border: 1px inset; 189 | vertical-align: middle; 190 | line-height: 50px; 191 | } 192 | 193 | .back-next { 194 | background: url("next.svg") no-repeat top left; 195 | background-size: contain; 196 | background-color: #333333; 197 | width: 32px; 198 | height: 32px; 199 | color: #FFFFFF; 200 | padding: 3px 2px 2px 2px; 201 | display: inline-block; 202 | border: 1px inset; 203 | font-size: 1.25em; 204 | vertical-align: middle; 205 | } 206 | 207 | #buttons input { 208 | outline: 0; 209 | } 210 | 211 | #buttons input:active { 212 | background-color: #555; 213 | } 214 | 215 | #keyboard-shortcuts { 216 | padding: 0; 217 | list-style-type: none; 218 | } 219 | 220 | #keyboard-shortcuts li { 221 | line-height: 1.75em; 222 | display: inline-block; 223 | padding-bottom: 0.5em; 224 | } 225 | 226 | .keyboard-shortcut { 227 | min-width: 1em; 228 | height: 1.5em; 229 | display: inline-block; 230 | font-family: Arial, "Helvetica Neue", Helvetica, sans-serif; 231 | background-color: #eee; 232 | border: 0.25em outset #c3c3c3; 233 | padding: 0 0.2em; 234 | vertical-align: middle; 235 | text-align: center; 236 | color: grey; 237 | } 238 | 239 | .mirror { 240 | display: inline-block; 241 | transform: scaleX(-1); 242 | } 243 | 244 | #select-buttons { 245 | margin-left: auto; 246 | height: 50px; 247 | display: flex; 248 | margin-right: 4px; 249 | } 250 | 251 | #select-buttons input { 252 | margin: auto; 253 | height: 32px; 254 | margin-left: 4px; 255 | cursor: pointer; 256 | } 257 | 258 | .selector { 259 | display: inline-block; 260 | font-family: Arial, "Helvetica Neue", Helvetica, sans-serif; 261 | letter-spacing: 0.5px; 262 | font-size: 0.7em; 263 | background-color: #333333; 264 | color: #FFFFFF; 265 | height: 32px; 266 | border: 1px inset; 267 | box-sizing: border-box; 268 | padding: 0 8px 0 4px; 269 | margin: auto; 270 | } 271 | 272 | .clear-frames { 273 | display: inline-block; 274 | font-family: Arial, "Helvetica Neue", Helvetica, sans-serif; 275 | letter-spacing: 0.5px; 276 | font-size: 0.7em; 277 | background-color: #333333; 278 | color: #FFFFFF; 279 | vertical-align: middle; 280 | border: 1px inset; 281 | padding: 2px 10px 2px 10px; 282 | } 283 | 284 | .selector input, .selector .selector-label, .selector label { 285 | vertical-align: middle; 286 | } 287 | 288 | select option { 289 | font-family: monospace; 290 | } 291 | 292 | .sm-title { 293 | font-weight: normal; 294 | font-size: 1.3em; 295 | margin: 0; 296 | padding-left: 0; 297 | display: inline-block; 298 | } 299 | 300 | .sketch-title { 301 | font-family: "Times New Roman", Times, serif; 302 | } 303 | 304 | .machine-title { 305 | font-family: monospace; 306 | font-size: 1.25em; 307 | } 308 | 309 | #controls { 310 | margin-top: 1.5em; 311 | font-family: "Times New Roman", Times, serif; 312 | color: #333; 313 | } 314 | 315 | #controls h2 { 316 | font-size: 1em; 317 | font-weight: normal; 318 | color: #777; 319 | margin: 0 0 6px 0; 320 | } 321 | 322 | .brush, .setting { 323 | background-color: #BBB; 324 | display: inline-block; 325 | /*border-radius: 4px;*/ 326 | margin-bottom: 5px; 327 | border: 1px inset; 328 | vertical-align: middle; 329 | height: 30px; /*40*/ 330 | line-height: 32px; /*40*/ 331 | padding-left: 6px; 332 | padding-right: 11px; 333 | padding-bottom: 2px; 334 | } 335 | 336 | .brush input, .brush select, .setting input, .setting .setting-label, .setting label { 337 | vertical-align: middle; 338 | } 339 | 340 | #engorge-label, #onion-label, #background-label, #select-label { 341 | cursor: pointer; 342 | } 343 | 344 | #select-label { 345 | padding-left: 4px; 346 | } 347 | 348 | #background-label { 349 | padding-right: 4px; 350 | } 351 | 352 | #tools { 353 | /*margin-top: 0.75em;*/ 354 | margin-bottom: 0.75em; 355 | } 356 | 357 | #tool-modifiers { 358 | margin-bottom: 0.75em; 359 | } 360 | 361 | #options { 362 | margin-bottom: 0.75em; 363 | } 364 | 365 | #playback-options label { 366 | padding: 3px; 367 | color: #666; 368 | display: inline-block; 369 | cursor: pointer; 370 | font-size: 1.25em; 371 | line-height: 18px; 372 | height: 20px; 373 | width: 20px; 374 | background: #999; 375 | } 376 | 377 | 378 | #playback-options .setting-label { 379 | padding-right: 4px; 380 | } 381 | 382 | #playback-options { 383 | padding-right: 6px; 384 | } 385 | 386 | #playback-options input[type="radio"] { 387 | display: none; 388 | } 389 | 390 | #playback-options input[type="radio"]:checked+label { 391 | color: #222; 392 | background: #eee; 393 | } 394 | 395 | #export-options { 396 | background-color: #BBB; 397 | padding: 8px; 398 | display: inline-block; 399 | border: 1px inset; 400 | } 401 | 402 | #export-overlay { 403 | position: fixed; 404 | z-index: 1; 405 | top: 0; 406 | left: 0; 407 | width: 100%; 408 | height: 100%; 409 | background-color: rgba(0, 0, 0, 0.75); 410 | visibility: hidden; 411 | display: flex; 412 | overflow-y: scroll; 413 | -webkit-overflow-scrolling: touch; 414 | } 415 | 416 | #export-overlay.active, 417 | #exported-gif-img.active, 418 | #export-controls.active { 419 | visibility: visible; 420 | } 421 | 422 | #exported-gif { 423 | margin: auto; 424 | } 425 | 426 | #exported-gif-spinner { 427 | position: absolute; 428 | top: 50%; 429 | left: 50%; 430 | width: 64px; 431 | height: 64px; 432 | margin-top: -32px; 433 | margin-left: -32px; 434 | background: white; 435 | border-radius: 50%; 436 | animation: spinner 1s infinite; 437 | pointer-events: none; 438 | transition: 0.5s border-radius; 439 | display: flex; 440 | } 441 | 442 | #export-progress { 443 | font-family: monospace; 444 | font-size: 1.5em; 445 | margin: auto; 446 | } 447 | 448 | #exported-gif-spinner.hidden { 449 | display: none; 450 | } 451 | 452 | @keyframes spinner { 453 | 0% { 454 | transform: scale(1); 455 | } 456 | 50% { 457 | transform: scale(1.5); 458 | } 459 | 100% { 460 | transform: scale(1); 461 | } 462 | } 463 | 464 | #exported-gif-img { 465 | margin: 8px; 466 | max-width: calc(100% - 24px); 467 | width: 512px; 468 | border: 4px outset; 469 | visibility: hidden; 470 | cursor: context-menu; 471 | image-rendering: pixelated; 472 | background: url(checkers.svg); 473 | background-size: 16px; 474 | } 475 | 476 | #export-controls { 477 | display: flex; 478 | visibility: hidden; 479 | margin: 0px 8px 8px 8px; 480 | } 481 | 482 | #export-upload-to-giphy { 483 | margin-left: auto; 484 | } 485 | 486 | #export-upload-to-giphy.hidden { 487 | visibility: hidden; 488 | } 489 | 490 | #export-upload-to-giphy, #export-close { 491 | display: block; 492 | font-size: 1.25em; 493 | color: black; 494 | font-weight: bold; 495 | background-color: #ddd; 496 | padding: 6px 8px 4px 8px; 497 | text-transform: uppercase; 498 | border: 4px outset; 499 | cursor: pointer; 500 | outline: none; 501 | } 502 | 503 | #export-upload-to-giphy:active, #export-close:active { 504 | border: 4px inset; 505 | padding: 7px 7px 3px 9px; 506 | background-color: #bbb; 507 | } 508 | 509 | #giphy-upload-overlay { 510 | visibility: hidden; 511 | z-index: 2; 512 | position: fixed; 513 | top: 0; 514 | left: 0; 515 | width: 100%; 516 | height: 100%; 517 | background: rgba(0, 0, 0, 0.8); 518 | } 519 | 520 | #giphy-upload-overlay.active { 521 | visibility: visible; 522 | } 523 | 524 | #giphy-logo { 525 | width: 100px; 526 | vertical-align: top; 527 | padding-left: 4px; 528 | padding-top: 1px; 529 | } 530 | 531 | #export-button { 532 | display: block; 533 | width: 77px; 534 | height: 77px; 535 | font-size: 1.5em; 536 | font-weight: bold; 537 | background-color: #dcdcdc; 538 | outline: none; 539 | color: #555; 540 | cursor: pointer; 541 | border: 0; 542 | border-radius: 0; 543 | font-family: "Times New Roman", Times, serif; 544 | padding: 0; 545 | } 546 | 547 | #export-button:active, #export-button.active { 548 | background-color: #aaa; 549 | color: #888; 550 | } 551 | 552 | #speed-slider { 553 | } 554 | 555 | #next { 556 | visibility: hidden; 557 | } 558 | 559 | #back { 560 | visibility: hidden; 561 | } 562 | 563 | #giphy-arts-logo { 564 | width: 100px; 565 | filter: grayscale(1); 566 | transition: 0.25s filter; 567 | } 568 | 569 | #giphy-arts-logo:hover, #giphy-arts-logo:active { 570 | filter: none; 571 | } 572 | 573 | @media all and (min-width: 940px) { 574 | body { 575 | } 576 | 577 | #ui { 578 | flex-direction: row; 579 | } 580 | 581 | #etc { 582 | flex-direction: column; 583 | padding: 10px; 584 | margin-left: 0; 585 | } 586 | 587 | #controls { 588 | padding-top: 0px; 589 | padding-left: 40px; /*16*/ 590 | margin-top: 0; 591 | } 592 | 593 | h1 { 594 | padding-left: 10px; 595 | } 596 | 597 | #colors button { 598 | width: 6.25%; 599 | padding: 6.25% 0 0 0; 600 | border: 0; 601 | /*outline: 0;*/ 602 | cursor: pointer; 603 | margin: 0; 604 | } 605 | } 606 | 607 | .dev * { 608 | background-color: rgba(0, 0, 255, 0.1); 609 | } -------------------------------------------------------------------------------- /src/timeline.js: -------------------------------------------------------------------------------- 1 | let selectLastFrame = false; 2 | let selectFirstFrame = false; 3 | let masterSelect = false; 4 | 5 | function selectRange (sketch) { 6 | 7 | masterSelect = !masterSelect; 8 | 9 | if (!masterSelect) { 10 | // First, deselect all 11 | deselect(); 12 | if (pause) { 13 | displayTimeline(timelineSketch); 14 | } 15 | } else { 16 | // First, deselect all 17 | deselect(); 18 | // Second, select the new range 19 | for (let i = firstFrame; i < lastFrame; i++) { 20 | onFrame[i] = true; 21 | } 22 | if (pause) { 23 | displayTimeline(timelineSketch); 24 | } 25 | } 26 | } 27 | 28 | function deselect (sketch) { 29 | for (let i = 0; i < numFrames; i++) { 30 | onFrame[i] = false; 31 | } 32 | } 33 | 34 | function manageSelection (sketch) { 35 | if (masterSelect) { 36 | deselect(); 37 | for (let i = firstFrame; i < lastFrame; i++) { 38 | onFrame[i] = true; 39 | } 40 | } 41 | } 42 | 43 | 44 | function displayTimeline (sketch) { 45 | 46 | sketch.drawingContext.clearRect(0, 0, sketch.width, sketch.height); 47 | 48 | let tw = frameDim / numFrames; 49 | let ty = 16; // Gap from the top of the canvas 50 | let tlh = 40; // Height of the time line -- Increased for better touch on mobile 51 | let th = 40; // Height of the selection arrows 52 | 53 | let tx = sketch.map(currentFrame, 0, numFrames, 0, sketch.width); 54 | 55 | if (!startDrawing) { 56 | for (let x = firstFrame; x < lastFrame; x++) { 57 | let xx = sketch.map(x, 0, numFrames, 0, sketch.width); 58 | if ((sketch.mouseX > xx && sketch.mouseX < xx + tw && sketch.mouseY > ty && sketch.mouseY < ty + tlh)) { 59 | if (sketch.mouseIsPressed && sketch.mouseY >= ty && sketch.mouseY <= ty + tlh && !selectFirstFrame && !selectLastFrame && !timelineRangeLock) { 60 | if (!pause) { 61 | clickPlay() 62 | } 63 | currentFrame = x; 64 | displayFrame(animationSketch); 65 | arrowLock = true; 66 | } 67 | } 68 | } 69 | } 70 | 71 | // DRAW FRAMES THAT ARE "ON", THAT ARE CURRENTLY BEING DRAWING INTO 72 | for (let i = firstFrame; i < lastFrame; i++) { 73 | if (onFrame[i]) { 74 | let tempx = sketch.map(i, 0, numFrames, 0, sketch.width); 75 | sketch.noStroke(); 76 | //sketch.fill(126, 126, 126); 77 | sketch.fill(0, 0, 255); 78 | sketch.rect(tempx, ty, tw, tlh + 1); 79 | } 80 | } 81 | 82 | // CURRENT FRAME MARKER IN BRIGHT BLUE 83 | sketch.noStroke(); 84 | //if (!masterSelect) { 85 | sketch.fill(0, 0, 255); 86 | //} else { 87 | // sketch.fill(255); 88 | //} 89 | sketch.rect(tx, ty, tw, tlh+1); 90 | 91 | // RANGE OF FRAMES, FIRST TO LAST 92 | let tty = ty+tlh+6; 93 | //let tty = ty; 94 | let ffx = firstFrame * tw; 95 | let lfx = (lastFrame - 1) * tw; 96 | 97 | // CONNECT IN AND OUT MARKERS 98 | sketch.stroke(102); 99 | sketch.line(firstFrame * tw + tw - 1, tty + th / 2, (lastFrame - 1) * tw, tty + th / 2); 100 | 101 | // IN MARKER 102 | sketch.fill(51); // Default color overwritten with blue if mouse is over 103 | sketch.noStroke(); 104 | if (sketch.mouseX > ffx && sketch.mouseX < ffx + tw && sketch.mouseY > tty && sketch.mouseY < tty + th) { 105 | if (!startDrawing) { 106 | if (sketch.mouseIsPressed && !selectLastFrame && !arrowLock && !timelineRangeLock) { 107 | selectFirstFrame = true; // Goes "false" in mouseReleased 108 | // 109 | } 110 | if (!selectLastFrame && !arrowLock) { 111 | sketch.fill(0, 0, 255); 112 | } 113 | } 114 | } 115 | if (selectFirstFrame) { 116 | firstFrame = sketch.floor(sketch.mouseX / tw); 117 | firstFrame = sketch.constrain(firstFrame, 0, lastFrame - 2); 118 | if (currentFrame < firstFrame) { 119 | currentFrame = firstFrame; 120 | } 121 | sketch.fill(0, 0, 255); 122 | manageSelection(); 123 | } 124 | sketch.triangle(firstFrame * tw, tty, firstFrame * tw, tty + th, (firstFrame + 1) * tw, tty + th / 2); 125 | 126 | // OUT MARKER 127 | sketch.fill(51); 128 | if (sketch.mouseX > lfx && sketch.mouseX < lfx + tw && sketch.mouseY > tty && sketch.mouseY < tty + th) { 129 | if (!startDrawing) { 130 | if (sketch.mouseIsPressed && !selectFirstFrame && !arrowLock && !timelineRangeLock) { 131 | selectLastFrame = true; // Goes "false" in mouseReleased 132 | } 133 | if (!selectFirstFrame && !arrowLock){ 134 | sketch.fill(0, 0, 255); 135 | } 136 | } 137 | } 138 | if (selectLastFrame) { 139 | lastFrame = sketch.ceil(sketch.mouseX / tw); 140 | lastFrame = sketch.constrain(lastFrame, firstFrame + 2, numFrames); 141 | if (currentFrame > lastFrame - 1) { 142 | currentFrame = lastFrame - 1; 143 | } 144 | sketch.fill(0, 0, 255); 145 | manageSelection(); 146 | } 147 | sketch.triangle(lastFrame * tw, tty, lastFrame * tw, tty + th, (lastFrame - 1) * tw, tty + th / 2); 148 | 149 | // BETWEEN THE IN AND OUT MARKER 150 | if (sketch.mouseX > ffx + tw && sketch.mouseX < lfx && sketch.mouseY > tty && sketch.mouseY < tty + th && !selectFirstFrame && !selectLastFrame) { 151 | if (!startDrawing && !arrowLock) { 152 | timelineRangeSelected = true; 153 | if (sketch.mouseIsPressed && !timelineRangeLock) { 154 | timelineRangeLock = true; 155 | let currentX = sketch.ceil(sketch.map(sketch.mouseX, 0, sketch.width, 0, numFrames)); 156 | numToLeft = currentX-firstFrame; 157 | numToRight = lastFrame-currentX; 158 | } 159 | } 160 | } else { 161 | timelineRangeSelected = false; 162 | } 163 | 164 | if (timelineRangeSelected || timelineRangeLock) { 165 | sketch.fill(153, 153, 153); 166 | sketch.noStroke(); 167 | sketch.rect(ffx, tty, lfx-ffx+tw, th); 168 | // Hack to draw the blue arrows on top, as well as the connecting line 169 | sketch.fill(0, 0, 255); 170 | sketch.triangle(lastFrame * tw, tty, lastFrame * tw, tty + th, (lastFrame - 1) * tw, tty + th / 2); 171 | sketch.triangle(firstFrame * tw, tty, firstFrame * tw, tty + th, (firstFrame + 1) * tw, tty + th / 2); 172 | sketch.stroke(102); 173 | sketch.line(firstFrame * tw + tw - 1, tty + th / 2, (lastFrame - 1) * tw, tty + th / 2); 174 | } 175 | 176 | // Calculate the "range" change if it's selected and locked 177 | if (timelineRangeLock) { 178 | 179 | //manageSelection(); 180 | 181 | let currentX = sketch.ceil(sketch.map(sketch.mouseX, 0, sketch.width, 0, numFrames)); 182 | let newX = sketch.map(currentX, 0, numFrames, 0, sketch.width); 183 | 184 | firstFrame = currentX - numToLeft; 185 | lastFrame = currentX + numToRight; 186 | 187 | manageSelection(); 188 | 189 | firstFrame = sketch.constrain(firstFrame, 0, numFrames-2); 190 | lastFrame = sketch.constrain(lastFrame, 2, numFrames); 191 | 192 | if (currentFrame < firstFrame) { 193 | currentFrame = firstFrame; 194 | } 195 | 196 | if (currentFrame > lastFrame-1) { 197 | currentFrame = lastFrame-1; 198 | } 199 | 200 | //sketch.print(currentX, firstFrame, lastFrame, numToLeft, numToRight); 201 | 202 | // Comment this bit out when it's working 203 | //sketch.stroke(255, 0, 0); 204 | //sketch.line(sketch.mouseX, 0, sketch.mouseX, sketch.height); 205 | //sketch.stroke(0, 255, 0); 206 | //sketch.line(newX, 0, newX, sketch.height); 207 | } 208 | 209 | // TICK MARKS, THE GRID OF FRAMES 210 | let tickMiddle = ty+tlh/2; 211 | for (let x = 0; x <= numFrames; x++) { 212 | 213 | let xx = sketch.map(x, 0, numFrames, 0, sketch.width); 214 | if (x === numFrames) { 215 | xx = xx-1; 216 | } 217 | if (x < firstFrame || x > lastFrame) { 218 | sketch.stroke(102); 219 | sketch.line(xx, tickMiddle-2, xx, tickMiddle+2); 220 | } else { 221 | sketch.stroke(0); 222 | sketch.line(xx, ty, xx, ty + tlh); 223 | } 224 | } 225 | 226 | } 227 | -------------------------------------------------------------------------------- /src/ui.js: -------------------------------------------------------------------------------- 1 | function speedSizeToggle() { 2 | speedSize = !speedSize; 3 | } 4 | 5 | function jitterToggle() { 6 | jitterOn = !jitterOn; 7 | } 8 | 9 | function sluggishToggle() { 10 | smoothing = !smoothing; 11 | } 12 | 13 | function onionToggle() { 14 | onionSkin = !onionSkin; 15 | displayFrame(animationSketch); 16 | } 17 | 18 | function backgroundToggle() { 19 | backgroundEnabled = !backgroundEnabled; 20 | displayFrame(animationSketch); 21 | } 22 | 23 | function openColorSelector (n) { 24 | document.body.classList.add('noscroll'); 25 | colorSelector.classList.add('active'); 26 | currentColorSelection = n; 27 | } 28 | 29 | function closeColorSelector (e) { 30 | currentColor = e.target.dataset.color 31 | 32 | if (currentColorSelection === 0) { 33 | backgroundColor = currentColor; 34 | displayFrame(animationSketch); 35 | backgroundColorButton.style.backgroundColor = currentColor; 36 | } else if (currentColorSelection === 1) { 37 | marker1Color = currentColor; 38 | marker1ColorButton.style.backgroundColor = marker1Color; 39 | } else if (currentColorSelection === 2) { 40 | marker2Color = currentColor; 41 | marker2ColorButton.style.backgroundColor = marker2Color; 42 | } else if (currentColorSelection === 3) { 43 | marker3Color = currentColor; 44 | marker3ColorButton.style.backgroundColor = marker3Color; 45 | } else if (currentColorSelection === 4) { 46 | marker4Color = currentColor; 47 | marker4ColorButton.style.backgroundColor = marker4Color; 48 | } else if (currentColorSelection === 5) { 49 | marker5Color = currentColor; 50 | marker5ColorButton.style.backgroundColor = marker5Color; 51 | } 52 | colorSelector.classList.remove('active'); 53 | document.body.classList.remove('noscroll'); 54 | } 55 | 56 | function clickPlay() { 57 | pause = !pause; 58 | if (pause) { 59 | displayFrame(animationSketch) 60 | //document.getElementById("play").value = "▶︎"; 61 | document.getElementById("play").style.backgroundImage = "url('play.svg')"; 62 | document.getElementById("next").style.visibility = "visible"; 63 | document.getElementById("back").style.visibility = "visible"; 64 | } else { 65 | //document.getElementById("play").value = "◼︎"; 66 | document.getElementById("play").style.backgroundImage = "url('pause.svg')"; 67 | document.getElementById("next").style.visibility = "hidden"; 68 | document.getElementById("back").style.visibility = "hidden"; 69 | if (playbackMode === BACKANDFORTH && currentFrame === lastFrame-1) { 70 | playbackDirection = -1; 71 | } 72 | } 73 | } 74 | 75 | function clickBack() { 76 | currentFrame--; 77 | if (currentFrame < firstFrame) { 78 | currentFrame = lastFrame - 1; 79 | } 80 | displayFrame(animationSketch); 81 | displayTimeline(timelineSketch); 82 | } 83 | 84 | function clickNext() { 85 | currentFrame++; 86 | if (currentFrame >= lastFrame) { 87 | currentFrame = firstFrame; 88 | } 89 | displayFrame(animationSketch); 90 | displayTimeline(timelineSketch); 91 | } 92 | 93 | function clickReverse() { 94 | playbackMode = REVERSE; 95 | } 96 | 97 | function clickForward() { 98 | playbackMode = FORWARD; 99 | } 100 | 101 | function clickBackAndForth() { 102 | if (playbackMode === REVERSE) { 103 | playbackDirection = -1 104 | } else if (playbackMode === FORWARD) { 105 | playbackDirection = 1 106 | } 107 | playbackMode = BACKANDFORTH; 108 | } 109 | 110 | function componentToHex(c) { 111 | let hex = c.toString(16); 112 | return hex.length === 1 ? "0" + hex : hex; 113 | } 114 | 115 | function rgbToHex(r, g, b) { 116 | return "#" + componentToHex(r) + componentToHex(g) + componentToHex(b); 117 | } 118 | 119 | const web216 = [ 120 | '#FFFFFF', 121 | '#000000', 122 | '#333333', 123 | '#666666', 124 | '#999999', 125 | '#CCCCCC', 126 | '#000033', 127 | '#000066', 128 | '#000099', 129 | '#0000CC', 130 | '#0000FF', 131 | '#003300', 132 | '#003333', 133 | '#003366', 134 | '#003399', 135 | '#0033CC', 136 | '#0033FF', 137 | '#006600', 138 | '#006633', 139 | '#006666', 140 | '#006699', 141 | '#0066CC', 142 | '#0066FF', 143 | '#009900', 144 | '#009933', 145 | '#009966', 146 | '#009999', 147 | '#0099CC', 148 | '#0099FF', 149 | '#00CC00', 150 | '#00CC33', 151 | '#00CC66', 152 | '#00CC99', 153 | '#00CCCC', 154 | '#00CCFF', 155 | '#00FF00', 156 | '#00FF33', 157 | '#00FF66', 158 | '#00FF99', 159 | '#00FFCC', 160 | '#00FFFF', 161 | '#330000', 162 | '#330033', 163 | '#330066', 164 | '#330099', 165 | '#3300CC', 166 | '#3300FF', 167 | '#333300', 168 | '#333366', 169 | '#333399', 170 | '#3333CC', 171 | '#3333FF', 172 | '#336600', 173 | '#336633', 174 | '#336666', 175 | '#336699', 176 | '#3366CC', 177 | '#3366FF', 178 | '#339900', 179 | '#339933', 180 | '#339966', 181 | '#339999', 182 | '#3399CC', 183 | '#3399FF', 184 | '#33CC00', 185 | '#33CC33', 186 | '#33CC66', 187 | '#33CC99', 188 | '#33CCCC', 189 | '#33CCFF', 190 | '#33FF00', 191 | '#33FF33', 192 | '#33FF66', 193 | '#33FF99', 194 | '#33FFCC', 195 | '#33FFFF', 196 | '#660000', 197 | '#660033', 198 | '#660066', 199 | '#660099', 200 | '#6600CC', 201 | '#6600FF', 202 | '#663300', 203 | '#663333', 204 | '#663366', 205 | '#663399', 206 | '#6633CC', 207 | '#6633FF', 208 | '#666600', 209 | '#666633', 210 | '#666699', 211 | '#6666CC', 212 | '#6666FF', 213 | '#669900', 214 | '#669933', 215 | '#669966', 216 | '#669999', 217 | '#6699CC', 218 | '#6699FF', 219 | '#66CC00', 220 | '#66CC33', 221 | '#66CC66', 222 | '#66CC99', 223 | '#66CCCC', 224 | '#66CCFF', 225 | '#66FF00', 226 | '#66FF33', 227 | '#66FF66', 228 | '#66FF99', 229 | '#66FFCC', 230 | '#66FFFF', 231 | '#990000', 232 | '#990033', 233 | '#990066', 234 | '#990099', 235 | '#9900CC', 236 | '#9900FF', 237 | '#993300', 238 | '#993333', 239 | '#993366', 240 | '#993399', 241 | '#9933CC', 242 | '#9933FF', 243 | '#996600', 244 | '#996633', 245 | '#996666', 246 | '#996699', 247 | '#9966CC', 248 | '#9966FF', 249 | '#999900', 250 | '#999933', 251 | '#999966', 252 | '#9999CC', 253 | '#9999FF', 254 | '#99CC00', 255 | '#99CC33', 256 | '#99CC66', 257 | '#99CC99', 258 | '#99CCCC', 259 | '#99CCFF', 260 | '#99FF00', 261 | '#99FF33', 262 | '#99FF66', 263 | '#99FF99', 264 | '#99FFCC', 265 | '#99FFFF', 266 | '#CC0000', 267 | '#CC0033', 268 | '#CC0066', 269 | '#CC0099', 270 | '#CC00CC', 271 | '#CC00FF', 272 | '#CC3300', 273 | '#CC3333', 274 | '#CC3366', 275 | '#CC3399', 276 | '#CC33CC', 277 | '#CC33FF', 278 | '#CC6600', 279 | '#CC6633', 280 | '#CC6666', 281 | '#CC6699', 282 | '#CC66CC', 283 | '#CC66FF', 284 | '#CC9900', 285 | '#CC9933', 286 | '#CC9966', 287 | '#CC9999', 288 | '#CC99CC', 289 | '#CC99FF', 290 | '#CCCC00', 291 | '#CCCC33', 292 | '#CCCC66', 293 | '#CCCC99', 294 | '#CCCCFF', 295 | '#CCFF00', 296 | '#CCFF33', 297 | '#CCFF66', 298 | '#CCFF99', 299 | '#CCFFCC', 300 | '#CCFFFF', 301 | '#FF0000', 302 | '#FF0033', 303 | '#FF0066', 304 | '#FF0099', 305 | '#FF00CC', 306 | '#FF00FF', 307 | '#FF3300', 308 | '#FF3333', 309 | '#FF3366', 310 | '#FF3399', 311 | '#FF33CC', 312 | '#FF33FF', 313 | '#FF6600', 314 | '#FF6633', 315 | '#FF6666', 316 | '#FF6699', 317 | '#FF66CC', 318 | '#FF66FF', 319 | '#FF9900', 320 | '#FF9933', 321 | '#FF9966', 322 | '#FF9999', 323 | '#FF99CC', 324 | '#FF99FF', 325 | '#FFCC00', 326 | '#FFCC33', 327 | '#FFCC66', 328 | '#FFCC99', 329 | '#FFCCCC', 330 | '#FFCCFF', 331 | '#FFFF00', 332 | '#FFFF33', 333 | '#FFFF66', 334 | '#FFFF99', 335 | '#FFFFCC' 336 | ]; 337 | 338 | // Populate color picker with buttons. 339 | 340 | const colors = document.querySelector('#colors'); 341 | 342 | web216.forEach((c) => { 343 | let btn = document.createElement('button'); 344 | btn.onclick = closeColorSelector; 345 | btn.dataset.color = c; 346 | btn.style.backgroundColor = c; 347 | colors.append(btn) 348 | }); 349 | 350 | // Resolution links 351 | 352 | document.querySelectorAll('a.res').forEach((el) => { 353 | if (el.href === window.location.href || surfaceDim === parseInt(el.innerText)) { 354 | el.classList.add('current-resolution'); 355 | } 356 | }); 357 | 358 | -------------------------------------------------------------------------------- /src/vendor/gif.js: -------------------------------------------------------------------------------- 1 | // gif.js 0.2.0 - https://github.com/jnordberg/gif.js 2 | (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)}); 3 | //# sourceMappingURL=gif.js.map 4 | -------------------------------------------------------------------------------- /src/vendor/gif.worker.js: -------------------------------------------------------------------------------- 1 | // gif.worker.js 0.2.0 - https://github.com/jnordberg/gif.js 2 | (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