├── .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 | 
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 |
307 |
316 |
317 | glitch image generator
318 |
319 |
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 |
425 |
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 | }
--------------------------------------------------------------------------------