├── .eslintrc.json ├── .gitignore ├── LICENSE ├── README.md ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── _app.js ├── api │ └── hello.js └── index.js ├── public ├── glitch-vintage.jpeg └── test-image.jpeg └── styles └── globals.css /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals", 3 | "rules": { 4 | "react/jsx-no-target-blank": "off" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Adam Fuhrer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Glitch Image Generator 2 | 3 | A generative tool which allows you to create and save unique glitchy images 4 | 5 | [glitchyimage.com](https://glitchyimage.com/) 6 | 7 | ![glitch art](https://glitchyimage.com/glitch-vintage.jpeg) 8 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | reactStrictMode: true, 3 | } 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "glitch-image", 3 | "private": true, 4 | "scripts": { 5 | "dev": "next dev", 6 | "build": "next build && next export", 7 | "start": "next start", 8 | "lint": "next lint" 9 | }, 10 | "dependencies": { 11 | "@emotion/react": "^11.7.1", 12 | "@emotion/styled": "^11.6.0", 13 | "@mui/material": "^5.2.4", 14 | "next": "12.0.3", 15 | "react": "17.0.2", 16 | "react-dom": "17.0.2" 17 | }, 18 | "devDependencies": { 19 | "eslint": "7.32.0", 20 | "eslint-config-next": "12.0.3" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import '../styles/globals.css' 2 | 3 | function MyApp({ Component, pageProps }) { 4 | return 5 | } 6 | 7 | export default MyApp 8 | -------------------------------------------------------------------------------- /pages/api/hello.js: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | 3 | export default function handler(req, res) { 4 | res.status(200).json({ name: 'John Doe' }) 5 | } 6 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | import Script from 'next/script'; 3 | import { useEffect, useState, useRef } from "react"; 4 | import Slider from '@mui/material/Slider'; 5 | import { createTheme, ThemeProvider } from '@mui/material'; 6 | 7 | var img; 8 | var canvas; 9 | var ctx; 10 | var sp = 25; 11 | var canvasWidth; 12 | var canvasHeight; 13 | var glitches = []; 14 | 15 | const theme = createTheme({ 16 | palette: { 17 | primary: { 18 | main: "#FF3ED4", 19 | }, 20 | }, 21 | }); 22 | 23 | function useAboutVisible(initialIsVisible) { 24 | const [isAboutVisible, setIsAboutVisible] = useState(initialIsVisible); 25 | const ref = useRef(null); 26 | 27 | const handleHide = (event) => { 28 | if (event.key === "Escape") { 29 | setIsAboutVisible(false); 30 | } 31 | }; 32 | 33 | const handleClickOutside = event => { 34 | if (ref.current && !ref.current.contains(event.target)) { 35 | setIsAboutVisible(false); 36 | } 37 | }; 38 | 39 | useEffect(() => { 40 | document.addEventListener("keydown", handleHide, true); 41 | document.addEventListener("click", handleClickOutside, true); 42 | return () => { 43 | document.removeEventListener("keydown", handleHide, true); 44 | document.removeEventListener("click", handleClickOutside, true); 45 | }; 46 | }); 47 | 48 | return { ref, isAboutVisible, setIsAboutVisible }; 49 | } 50 | 51 | export default function Home() { 52 | const blendingModes = [ 53 | // "source-in", 54 | // "source-out", 55 | "difference", 56 | "source-atop", 57 | // "destination-over", 58 | // "destination-in", 59 | "destination-out", 60 | // "destination-atop", 61 | "lighter", 62 | // "copy", 63 | // "xor", 64 | "multiply", 65 | "screen", 66 | "overlay", 67 | "darken", 68 | "lighten", 69 | "color-dodge", 70 | "color-burn", 71 | "hard-light", 72 | "soft-light", 73 | "exclusion", 74 | "hue", 75 | "color", 76 | "luminosity" 77 | ]; 78 | 79 | const canvasRef = useRef(null) 80 | const fileUploadRef = useRef(null) 81 | const [blendingMode, setBlendingMode] = useState("difference"); 82 | const [opacity, setOpacity] = useState(1); 83 | const [amountOfGlitches, setAmountOfGlitches] = useState(60); 84 | const [maxAmountOfGlitches, setMaxAmountOfGlitches] = useState(400); 85 | const [imgSrc, setImgSrc] = useState("/test-image.jpeg"); 86 | const [imgHeight, setImgHeight] = useState(0); 87 | const { ref, isAboutVisible, setIsAboutVisible } = useAboutVisible(false); 88 | const [isImageLoaded, setIsImageLoaded] = useState(false); 89 | 90 | useEffect(() => { 91 | onGenerateClick(true); 92 | }, [imgSrc, amountOfGlitches]); 93 | 94 | useEffect(() => { 95 | onGenerateClick(); 96 | }, [opacity, blendingMode]); 97 | 98 | class Glitch { 99 | constructor(sourceX, sourceY, glitchWidth, glitchHeight, destinationX) { 100 | this.ratio = window.devicePixelRatio || 1; 101 | this.sourceX = sourceX; 102 | this.sourceY = sourceY; 103 | this.glitchWidth = glitchWidth; 104 | this.glitchHeight = glitchHeight 105 | this.destinationX = destinationX; 106 | } 107 | 108 | draw = () => { 109 | ctx.drawImage( 110 | canvas, 111 | this.sourceX * this.ratio, // Scale up the source image by the devicePixelRatio 112 | this.sourceY * this.ratio, 113 | this.glitchWidth * this.ratio, 114 | this.glitchHeight * this.ratio, 115 | this.destinationX, 116 | this.sourceY, 117 | this.glitchWidth, 118 | this.glitchHeight 119 | ) 120 | } 121 | } 122 | 123 | function setupGlitches() { 124 | glitches = []; 125 | let sourceY = 0; 126 | let glitchHeight = canvasHeight / amountOfGlitches; 127 | 128 | for (let i = 0; i < amountOfGlitches; i++) { 129 | let sourceX = randomNum(0, canvasWidth / 2); 130 | let glitchWidth = randomNum(canvasWidth * 0.3, canvasWidth); 131 | let destinationX = randomNum(0, canvasWidth / 1.75); 132 | 133 | glitches[i] = new Glitch(sourceX, sourceY, glitchWidth, glitchHeight, destinationX) 134 | sourceY = sourceY + Number(glitchHeight); 135 | } 136 | } 137 | 138 | function onGenerateClick(setup = false) { 139 | canvas = canvasRef.current; 140 | img = document.createElement("img"); 141 | img.src = imgSrc; 142 | 143 | if (img.complete) { 144 | processImg() 145 | } else { 146 | img.onload = processImg; 147 | } 148 | 149 | function processImg() { 150 | setIsImageLoaded(true) 151 | const dimensions = resizeImg(img); 152 | canvasWidth = dimensions.width; 153 | canvasHeight = dimensions.height; 154 | img.width = canvasWidth; 155 | img.height = canvasHeight; 156 | 157 | if (setup) { 158 | setupGlitches(); 159 | } 160 | setMaxAmountOfGlitches(Math.round(canvasHeight / 2)) 161 | setImgHeight(canvasHeight); 162 | 163 | // Account for high HiDPI screens, otherwise images drawn on canvas will be slightly blurry 164 | let ratio = window.devicePixelRatio || 1; 165 | canvas = canvasRef.current; 166 | canvas.width = canvasWidth * ratio; 167 | canvas.height = canvasHeight * ratio; 168 | canvas.style.width = canvasWidth + 'px'; 169 | canvas.style.height = canvasHeight + 'px'; 170 | 171 | ctx = canvas.getContext("2d"); 172 | ctx.scale(ratio, ratio); 173 | ctx.drawImage(img, 0, 0, canvasWidth, canvasHeight); 174 | ctx.globalAlpha = Number(opacity); 175 | ctx.globalCompositeOperation = blendingMode; 176 | ctx.imageSmoothingEnabled = false; 177 | 178 | // Draw glitches on the canvas 179 | glitches.forEach(glitch => { 180 | glitch.draw(); 181 | }); 182 | } 183 | } 184 | 185 | function onDownloadClick() { 186 | let link = document.createElement('a') 187 | 188 | canvas.toBlob(function(blob) { 189 | link.setAttribute( 190 | 'download', 191 | 'glitch-image-' + Math.round(new Date().getTime() / 1000) + '.png' 192 | ) 193 | 194 | link.setAttribute('href', URL.createObjectURL(blob)) 195 | link.dispatchEvent( 196 | new MouseEvent('click', { 197 | bubbles: true, 198 | cancelable: true, 199 | view: window, 200 | }) 201 | ) 202 | }) 203 | } 204 | 205 | function loadImage() { 206 | let input = fileUploadRef.current; 207 | 208 | function handleChange(e) { 209 | for (let item of this.files) { 210 | if (item.type.indexOf('image') < 0) { 211 | continue 212 | } 213 | let src = URL.createObjectURL(item) 214 | setImgSrc(src); 215 | this.removeEventListener('change', handleChange) 216 | } 217 | } 218 | 219 | input.addEventListener('change', handleChange) 220 | input.dispatchEvent( 221 | new MouseEvent('click', { 222 | bubbles: true, 223 | cancelable: true, 224 | view: window, 225 | }) 226 | ) 227 | } 228 | 229 | function handleCompositeOperationChange(event) { 230 | setBlendingMode(event.target.value); 231 | } 232 | 233 | function handleOpacityChange(event) { 234 | if (event.target.value > 1) { 235 | setOpacity(1); 236 | } else { 237 | setOpacity(Number(event.target.value)); 238 | } 239 | } 240 | 241 | function handleGlitchesAmountChange(event) { 242 | if (event.target.value > canvasHeight) { 243 | setAmountOfGlitches(Number(canvasHeight)); 244 | } else { 245 | setAmountOfGlitches(Number(event.target.value)) 246 | } 247 | } 248 | 249 | // Image resizing from: https://github.com/constraint-systems/collapse/blob/master/pages/index.js 250 | function resizeImg(img) { 251 | let aspect = img.width / img.height; 252 | let window_aspect = (window.innerWidth - sp) / (window.innerHeight - sp * 8); 253 | let width, height; 254 | if (aspect < window_aspect) { 255 | let adj_height = Math.min( 256 | img.height, 257 | Math.floor(window.innerHeight - sp * 8) 258 | ) 259 | height = Math.round(adj_height / sp) * sp 260 | let snapr = height / img.height 261 | width = Math.round((img.width * snapr) / sp) * sp 262 | } else { 263 | let adj_width = Math.min( 264 | img.width, 265 | Math.floor(window.innerWidth - sp) - sp / 2 266 | ) 267 | width = Math.round(adj_width / sp) * sp 268 | let snapr = width / img.width 269 | height = Math.round((img.height * snapr) / sp) * sp 270 | } 271 | return { width, height } 272 | } 273 | 274 | function randomNum(min, max) { 275 | return Math.floor(Math.random() * (max - min + 1) + min) 276 | } 277 | 278 | return ( 279 |
280 | 281 | 282 | 283 | Glitch Image Generator 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 |
303 | 316 | 317 |

glitch image generator

318 |
319 |
320 |
mode
321 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 |
339 | 340 |
341 |
amount
342 | 349 | 356 |
357 | 358 |
359 |
opacity
360 | 368 | 376 |
377 |
378 | 379 | 385 |
386 | 394 | 402 | 410 |
411 | 412 | 417 | {isAboutVisible && 418 |
419 |
420 | a generative tool which allows you to create and save unique glitchy images 421 |
422 |
423 | another generative glitch tool: glitchart.io 424 |
425 |
426 | project by adam fuhrer 427 |
428 | 437 |
438 | } 439 |
440 |
441 |
442 | ) 443 | } 444 | -------------------------------------------------------------------------------- /public/glitch-vintage.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamfuhrer/glitch-image/eb4d5a44972391b6cac8a40906fc2e9a53a08e39/public/glitch-vintage.jpeg -------------------------------------------------------------------------------- /public/test-image.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamfuhrer/glitch-image/eb4d5a44972391b6cac8a40906fc2e9a53a08e39/public/test-image.jpeg -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --text-shadow: 0 1px rgb(0 0 0 / 30%); 3 | --box-shadow: 0px 3px 1px -2px rgb(0 0 0 / 20%), 0px 2px 2px 0px rgb(0 0 0 / 14%), 0px 1px 5px 0px rgb(0 0 0 / 12%); 4 | --pink: #FF3ED4; 5 | --white-background: rgba(255, 255, 255, 0.2); 6 | --white-border: rgba(255, 255, 255, 0.3); 7 | --input-color: #52555F; 8 | --controls-max-width: 1000px; 9 | } 10 | 11 | * { 12 | box-sizing: border-box; 13 | } 14 | 15 | html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video { 16 | margin: 0; 17 | padding: 0; 18 | border: 0; 19 | font-size: 100%; 20 | font-family: inherit; 21 | vertical-align: baseline; 22 | } 23 | 24 | html { 25 | font-size: 62.5%; 26 | box-sizing: border-box; 27 | background: #272a37; 28 | } 29 | 30 | *, *:before, *:after { 31 | box-sizing: inherit; 32 | } 33 | 34 | main, article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section { 35 | display: block; 36 | } 37 | 38 | input:focus, 39 | select:focus, 40 | textarea:focus { 41 | outline: none; 42 | } 43 | 44 | input, select { 45 | font-size: 14px; 46 | box-shadow: var(--text-shadow) 47 | } 48 | 49 | input:hover, select:hover { 50 | cursor: pointer; 51 | } 52 | 53 | body { 54 | font-family: 'Fira Code', monospace; 55 | height: 100%; 56 | font-size: 14px; 57 | line-height: 1.5; 58 | color: white; 59 | } 60 | 61 | canvas { 62 | opacity: 0; 63 | } 64 | 65 | canvas.loaded { 66 | opacity: 1; 67 | transition: opacity 0.3s ease; 68 | box-shadow: var(--box-shadow); 69 | } 70 | 71 | body { 72 | margin: 0; 73 | font-family: 'Fira Code', monospace; 74 | } 75 | 76 | main { 77 | display: flex; 78 | flex-direction: column; 79 | justify-content: center; 80 | align-items: center; 81 | width: 100%; 82 | margin-bottom: 46px; 83 | } 84 | 85 | .controls { 86 | display: flex; 87 | margin-top: 20px; 88 | margin-bottom: 20px; 89 | width: 100%; 90 | max-width: var(--controls-max-width); 91 | padding-left: 20px; 92 | padding-right: 20px; 93 | } 94 | 95 | .buttons-wrapper { 96 | bottom: 0; 97 | left: 0; 98 | right: 0; 99 | height: 40px; 100 | display: flex; 101 | justify-content: center; 102 | align-items: center; 103 | background: var(--input-color); 104 | display: flex; 105 | position: fixed; 106 | } 107 | 108 | button { 109 | font-family: 'Fira Code', monospace; 110 | display: flex; 111 | align-items: center; 112 | justify-content: center; 113 | height: 40px; 114 | background: transparent; 115 | color: white; 116 | font-weight: 500; 117 | box-shadow: none; 118 | border: none; 119 | border-radius: 3px; 120 | font-size: 14px; 121 | transition: color 0.2s ease; 122 | text-shadow: var(--text-shadow); 123 | } 124 | 125 | button:hover, 126 | a:hover { 127 | cursor: pointer; 128 | color: var(--pink); 129 | transition: color 0.2s ease; 130 | } 131 | 132 | a { 133 | color: white; 134 | transition: color 0.2s ease; 135 | } 136 | 137 | button.main-button { 138 | padding-left: 18px; 139 | padding-right: 18px; 140 | } 141 | 142 | button.main-button svg { 143 | width: 18px; 144 | height: 18px; 145 | margin-right: 8px; 146 | } 147 | 148 | a.info-button { 149 | height: 24px; 150 | min-width: 24px; 151 | display: flex; 152 | align-items: center; 153 | justify-content: center; 154 | margin-left: 4px; 155 | } 156 | 157 | a.info-button svg { 158 | width: 16px; 159 | height: 16px; 160 | } 161 | 162 | button + button { 163 | margin-left: 20px; 164 | } 165 | 166 | .input-wrapper { 167 | display: flex; 168 | align-items: center; 169 | flex: 1; 170 | } 171 | 172 | .input-wrapper + .input-wrapper { 173 | margin-left: 32px; 174 | } 175 | 176 | .input-wrapper.glitches-amount { 177 | flex: 1.5; 178 | } 179 | 180 | .input { 181 | font-family: 'Fira Code', monospace; 182 | height: 32px; 183 | outline: none; 184 | padding-left: 10px; 185 | padding-right: 10px; 186 | border-radius: 5px; 187 | background: var(--input-color); 188 | border: 1px solid var(--white-border); 189 | color: white; 190 | } 191 | 192 | h1 { 193 | font-family: 'Press Start 2P', sans-serif; 194 | text-align: center; 195 | font-size: 20px; 196 | margin-top: 26px; 197 | padding-left: 30px; 198 | padding-right: 30px; 199 | margin-bottom: 6px; 200 | color: white; 201 | text-shadow: var(--text-shadow); 202 | letter-spacing: 1px; 203 | } 204 | 205 | .label { 206 | font-size: 14px; 207 | margin-right: 14px; 208 | font-weight: 500; 209 | color: white; 210 | text-shadow: var(--text-shadow) 211 | } 212 | 213 | .number-input { 214 | min-width: 70px; 215 | margin-left: 16px; 216 | } 217 | 218 | #file-input { 219 | display: none; 220 | } 221 | 222 | select { 223 | width: 100%; 224 | -webkit-appearance: none; 225 | -moz-appearance: none; 226 | } 227 | 228 | .about-button { 229 | position: absolute; 230 | top: 16px; 231 | right: 16px; 232 | border-radius: 50%; 233 | background: var(--input-color); 234 | color: white; 235 | width: 30px; 236 | height: 30px; 237 | box-shadow: var(--box-shadow); 238 | } 239 | 240 | .about-button svg { 241 | min-width: 20px; 242 | min-height: 20px; 243 | } 244 | 245 | .about-section { 246 | position: absolute; 247 | top: 56px; 248 | right: 16px; 249 | padding: 24px; 250 | width: 280px; 251 | background: var(--input-color); 252 | border-radius: 8px; 253 | font-weight: 500; 254 | box-shadow: var(--box-shadow); 255 | } 256 | 257 | .about-section div + div { 258 | margin-top: 18px; 259 | } 260 | 261 | .about-section a { 262 | text-decoration: none; 263 | color: var(--pink); 264 | } 265 | 266 | .about-section a:hover { 267 | color: #ff89e5; 268 | } 269 | 270 | .github-link { 271 | display: inline-flex; 272 | color: white; 273 | } 274 | 275 | .github-link svg { 276 | fill: white; 277 | width: 18px; 278 | height: 18px; 279 | transition: fill 0.2s ease; 280 | } 281 | 282 | .github-link:hover svg { 283 | fill: var(--pink); 284 | transition: fill 0.2s ease; 285 | } 286 | 287 | @media (max-width: 840px) { 288 | .controls { 289 | flex-direction: column; 290 | } 291 | 292 | .input-wrapper + .input-wrapper { 293 | margin-left: 0px; 294 | margin-top: 18px; 295 | } 296 | } 297 | 298 | @media (max-width: 500px) { 299 | .main-button span { 300 | display: none; 301 | } 302 | } --------------------------------------------------------------------------------