├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs-assets ├── screenshot.jpg └── thumbnail.jpg ├── package-lock.json ├── package.json ├── public ├── img2css.jpg └── index.html └── src ├── app.jsx └── index.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | [*.md] 11 | max_line_length = off 12 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | }, 6 | extends: ['eslint:recommended', 'plugin:react/recommended'], 7 | parserOptions: { 8 | ecmaFeatures: { 9 | jsx: true, 10 | }, 11 | ecmaVersion: 12, 12 | sourceType: 'module', 13 | }, 14 | plugins: ['react'], 15 | rules: {}, 16 | }; 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /dist 5 | .vercel 6 | /node_modules 7 | /.pnp 8 | .pnp.js 9 | 10 | # testing 11 | /coverage 12 | 13 | # production 14 | /build 15 | 16 | # misc 17 | .DS_Store 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "printWidth": 80, 4 | "editor.formatOnSave": true, 5 | "proseWrap": "always", 6 | "tabWidth": 2, 7 | "requireConfig": false, 8 | "useTabs": false, 9 | "trailingComma": "es5", 10 | "bracketSpacing": true, 11 | "jsxBracketSameLine": false, 12 | "semi": true 13 | } 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.1.0 2 | 3 | - Add base64 output option 4 | - Replace internal components for JBX 5 | 6 | # 1.0.0 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, Javier Bórquez 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [img2css](https://javier.xyz/img2css/) 2 | 3 | Convert any image to pure CSS. 4 | 5 | [![img2css](public/img2css.jpg)](https://javier.xyz/img2css/) 6 | 7 | - To use it go to https://javier.xyz/img2css 8 | - Looking for a programmatic way to do this? See https://github.com/javierbyte/canvas-image-utils 9 | - I also made a per-pixel animation experiment, see https://github.com/javierbyte/morphin 10 | 11 | ## How does it work? 12 | 13 | It has two different outputs, pure css shadow matrix [1] and base64 embedded image [2]. 14 | 15 | - **Pure CSS**: This output was created by resizing and setting each pixel as a box-shadow of a single pixel div, so no `img` tag or `background-image` is needed. This can result in huge outputs, and the use of this output is not recommended for production unless there is no other option. 16 | - **Base64**: The entire image file is embedded inside the `` tag using base64, so no need external hosting is needed. 17 | 18 | ### Development 19 | 20 | Run development server: 21 | 22 | ``` 23 | npm run dev 24 | ``` 25 | 26 | Build 27 | 28 | ``` 29 | npm run build 30 | ``` 31 | -------------------------------------------------------------------------------- /docs-assets/screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javierbyte/img2css/441c7c968f0a659b48392c8cf7fd61b9f854d329/docs-assets/screenshot.jpg -------------------------------------------------------------------------------- /docs-assets/thumbnail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javierbyte/img2css/441c7c968f0a659b48392c8cf7fd61b9f854d329/docs-assets/thumbnail.jpg -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "img2css", 3 | "version": "1.2.0", 4 | "description": "Convert any image to pure css.", 5 | "scripts": { 6 | "dev": "react-scripts start", 7 | "build": "react-scripts build", 8 | "test": "react-scripts test", 9 | "eject": "react-scripts eject", 10 | "predeploy": "npm run build", 11 | "deploy": "gh-pages -d build" 12 | }, 13 | "homepage": "https://javier.xyz/img2css/", 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/javierbyte/img2css.git" 17 | }, 18 | "keywords": [ 19 | "img2css", 20 | "images", 21 | "css", 22 | "experiments", 23 | "pixelart" 24 | ], 25 | "author": "Javier Bórquez (http://github.com/javierbyte)", 26 | "license": "BSD-3-Clause", 27 | "devDependencies": { 28 | "gh-pages": "^3.2.3" 29 | }, 30 | "dependencies": { 31 | "canvas-image-utils": "^2.1.1", 32 | "capsize": "^2.0.0", 33 | "jbx": "^1.7.6", 34 | "lodash": "^4.17.20", 35 | "react": "^18.0.0", 36 | "react-dom": "^18.0.0", 37 | "react-scripts": "^5.0.0", 38 | "styled-components": "^5.3.5", 39 | "tinycolor2": "^1.4.2" 40 | }, 41 | "browserslist": { 42 | "production": [ 43 | ">0.2%", 44 | "not dead", 45 | "not op_mini all" 46 | ], 47 | "development": [ 48 | "last 1 chrome version", 49 | "last 1 firefox version", 50 | "last 1 safari version" 51 | ] 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /public/img2css.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javierbyte/img2css/441c7c968f0a659b48392c8cf7fd61b9f854d329/public/img2css.jpg -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | img2css | Convert any image to pure CSS 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 37 | 38 | 44 | 82 | 83 | 84 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /src/app.jsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useState } from 'react'; 2 | import tinycolor from 'tinycolor2'; 3 | import _ from 'lodash'; 4 | 5 | import { 6 | JBX, 7 | MainHeader, 8 | Text, 9 | Space, 10 | A, 11 | Container, 12 | Dropzone, 13 | Ul, 14 | Li, 15 | Tabs, 16 | Tab, 17 | Inline, 18 | } from 'jbx'; 19 | 20 | import Styled from 'styled-components'; 21 | 22 | import { imageToRGBMatrix, imageToRawData } from 'canvas-image-utils'; 23 | 24 | const Textarea = Styled.textarea({ 25 | fontFamily: 'monaco, monospace', 26 | border: 'none', 27 | width: '100%', 28 | height: 256, 29 | fontSize: 13, 30 | lineHeight: 1.309, 31 | padding: '16px 18px', 32 | background: '#ecf0f1', 33 | color: '#34495e', 34 | ':focus': { 35 | outline: 'none', 36 | }, 37 | }); 38 | 39 | function compressColor(rgb) { 40 | const hex = tinycolor(rgb).toHexString(); 41 | 42 | switch ( 43 | hex // based on CSS3 supported color names http://www.w3.org/TR/css3-color/ 44 | ) { 45 | case '#c0c0c0': 46 | return 'silver'; 47 | case '#808080': 48 | return 'gray'; 49 | case '#800000': 50 | return 'maroon'; 51 | case '#ff0000': 52 | return 'red'; 53 | case '#800080': 54 | return 'purple'; 55 | case '#008000': 56 | return 'green'; 57 | case '#808000': 58 | return 'olive'; 59 | case '#000080': 60 | return 'navy'; 61 | case '#008080': 62 | return 'teal'; 63 | } 64 | return hex[1] === hex[2] && hex[3] === hex[4] && hex[5] === hex[6] 65 | ? '#' + hex[1] + hex[3] + hex[5] 66 | : hex; 67 | } 68 | 69 | function App() { 70 | const [outputType, outputTypeSet] = useState('SHADOW'); 71 | const [originalSize, originalSizeSet] = useState(0); 72 | const [base64Data, base64DataSet] = useState(''); 73 | const [rgbMatrix, rgbMatrixSet] = useState(null); 74 | const [loadingImage, loadingImageSet] = useState(false); 75 | 76 | function onFileSelected(event) { 77 | event.stopPropagation(); 78 | event.preventDefault(); 79 | 80 | const dt = event.dataTransfer; 81 | const files = dt ? dt.files : event.target.files; 82 | const file = files[0]; 83 | 84 | originalSizeSet(file.size); 85 | 86 | const fr = new window.FileReader(); 87 | 88 | loadingImageSet(true); 89 | 90 | fr.onload = async (data) => { 91 | const base64src = data.currentTarget.result; 92 | const dataMatrix = await imageToRGBMatrix(base64src, { size: 200 }); 93 | const canvasRawData = await imageToRawData(base64src, { 94 | size: 1080, 95 | crop: false, 96 | }); 97 | 98 | base64DataSet(canvasRawData.ctx.canvas.toDataURL('image/jpeg', 0.75)); 99 | rgbMatrixSet(dataMatrix); 100 | loadingImageSet(false); 101 | }; 102 | fr.readAsDataURL(file); 103 | } 104 | 105 | function onDragOver(event) { 106 | event.stopPropagation(); 107 | event.preventDefault(); 108 | } 109 | 110 | let scale = 1; 111 | 112 | const masterShadow = _.map(rgbMatrix, (row, rowIndexSrc) => { 113 | return _.map(row, (col, colIndexSrc) => { 114 | const i = colIndexSrc * scale; 115 | const j = rowIndexSrc * scale; 116 | 117 | const color = compressColor(`rgb(${col.r},${col.g},${col.b})`); 118 | 119 | const scaleCompensation = scale !== 1 ? ` 0 ${scale / 2}px` : ``; 120 | 121 | return `${color} ${j ? j + 'px' : 0} ${ 122 | i ? i + 'px' : 0 123 | }${scaleCompensation}`; 124 | }).join(','); 125 | }).join(','); 126 | 127 | const handleFocus = (event) => { 128 | event.preventDefault(); 129 | event.stopPropagation(); 130 | event.target.select(); 131 | }; 132 | 133 | return ( 134 | 135 | 136 | img2css 137 | 138 | 139 | 140 | This is a tool that can convert any image into a pure css image. 141 | 142 | 143 | 144 | 149 | {loadingImage ? ( 150 | Processing... 151 | ) : ( 152 |
153 | Drop an image here 154 | or click to select 155 |
156 | )} 157 | 164 |
165 | 166 | 167 | 168 | I also made a per-pixel animation experiment using the box-shadow idea, 169 | see morphin. 170 | 171 | 172 | {rgbMatrix && ( 173 | 174 | 175 | 176 | 177 | { 181 | outputTypeSet('SHADOW'); 182 | }} 183 | > 184 | {'Pure CSS'} 185 | 186 | { 190 | outputTypeSet('BASE64'); 191 | }} 192 | > 193 | {'Base64'} 194 | 195 | 196 | 197 | 198 | 199 | {outputType === 'BASE64' && ( 200 | 201 | 202 | The result (base64).{' '} 203 | { 204 | 'This is your image tag a base64 output. The entire image file is embedded inside the `` tag using base64, so no need external hosting is needed.' 205 | } 206 | 207 | 208 | 209 | 213 | 214 | 215 | 216 |