Skip to content

Instantly share code, notes, and snippets.

@FelixRilling
Last active September 14, 2021 07:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save FelixRilling/82a6c6efad4be263ae43ea9d8e2f23a3 to your computer and use it in GitHub Desktop.
Save FelixRilling/82a6c6efad4be263ae43ea9d8e2f23a3 to your computer and use it in GitHub Desktop.
box-shadow-image encoder
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta content="width=device-width, initial-scale=1" name="viewport" />
<title>Box Shadow Image</title>
<style>
*,
::before,
::after {
box-sizing: border-box;
}
body {
display: grid;
height: 100vh;
margin: 0;
grid-template-columns: 1fr 1.5fr;
grid-template-rows: 1fr 1fr;
grid-template-areas:
"input demo"
"output demo";
}
.section {
padding: 1rem;
}
.input {
grid-area: input;
}
.output {
grid-area: output;
}
.demo {
margin: 1rem;
border: 1px solid #999999;
grid-area: demo;
}
#demo {
width: 1px;
height: 1px;
}
</style>
</head>
<body>
<div class="section input">
<h1>Input</h1>
<label for="fileUpload"
>Upload File (Warning: Large images may freeze browser!):</label
>
<input accept="image/*" id="fileUpload" type="file" />
<p>Example images:</p>
<ul>
<li>
<a
href="https://commons.wikimedia.org/wiki/File:AdditiveColor.svg"
rel="noopener"
target="_blank"
>Image with primitive shapes and few different
colors.</a
>
</li>
<li>
<a
href="https://commons.wikimedia.org/wiki/File:PNG_transparency_demonstration_1.png"
rel="noopener"
target="_blank"
>Image with full and partial transparency.</a
>
</li>
<li>
<a
href="https://commons.wikimedia.org/wiki/File:Mona_Lisa,_by_Leonardo_da_Vinci,_from_C2RMF_retouched.jpg"
rel="noopener"
target="_blank"
>Image with a lot of different colors and complex
shapes.</a
>
</li>
</ul>
<canvas hidden id="canvas"></canvas>
</div>
<div class="section output">
<h2>Output</h2>
<p>
<label for="output"
>Output <code>box-shadow</code> value:</label
>
</p>
<textarea cols="70" id="output" readonly rows="20"></textarea>
</div>
<div class="section demo">
<div id="demo"></div>
</div>
<script type="module">
import { initBoxShadowImage } from "./main.js";
initBoxShadowImage();
</script>
</body>
</html>
const readFileAsDataUrl = (file: File): Promise<string> =>
new Promise((resolve, reject) => {
const fileReader = new FileReader();
fileReader.onload = () => resolve(fileReader.result as string);
fileReader.onerror = () => reject(fileReader.error);
fileReader.readAsDataURL(file);
});
const prepareCanvas = (
canvas: HTMLCanvasElement,
image: CanvasImageSource
): void => {
const { height, width } = image;
if (
height instanceof SVGAnimatedLength ||
width instanceof SVGAnimatedLength
) {
throw new TypeError("Unsupported source.");
}
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext("2d")!;
ctx.clearRect(0, 0, width, height);
ctx.drawImage(image, 0, 0);
};
/**
* Serializes px value as CSS size.
*
* @param number Pixel value.
*/
const pxAsString = (number: number): string => {
// Optimization: omit 'px' if 0.
if (number == 0) {
return "0";
}
return `${number}px`;
};
// Used for documentation purposes, not enforced by type checks.
// eslint-disable-next-line @typescript-eslint/naming-convention
type u8 = number;
// Encode u8 number as hexadecimal, padding to two digits if needed.
const encodeAsHexPair = (val: u8): string => {
let str = val.toString(16);
if (str.length === 1) {
str = "0" + str; // Pad single digits
}
return str;
};
// Channels where both hex digits are the same (like 'FF') can be shorthanded.
const canShorthandChannel = (hexPair: string): boolean =>
hexPair[0] == hexPair[1];
// See canShorthandChannel
const shorthandChannel = (hexPair: string): string => hexPair[0];
/**
* Serializes RGB channel values as CSS color.
*
* @param r Red channel integer from 0 to 255
* @param g Green channel integer from 0 to 255
* @param b Blue channel integer from 0 to 255
* @param a Alpha channel integer from 0 to 255
*/
// Note: floating point numbers do not need handling as ImageData only contains u8 numbers.
const rgbAsString = (r: u8, g: u8, b: u8, a: u8): string => {
let channelValues = [r, g, b, a].map(encodeAsHexPair);
// Optimization: Use shorthand notation where fitting.
if (channelValues.every(canShorthandChannel)) {
channelValues = channelValues.map(shorthandChannel);
}
const [rHex, gHex, bHex, aHex] = channelValues;
let color = `#${rHex}${gHex}${bHex}`;
// Optimization: Is color fully opaque? If yes, we can omit the alpha channel
if (a != 255) {
color += aHex;
}
return color;
};
/*
* Possible optimisations:
* - Only use one shadow for adjacent pixels in the same color.
* - Stop using this and use images like normal people.
*/
const createBoxShadowImageFromCanvas = (
preparedCanvas: HTMLCanvasElement
): string => {
const { height, width } = preparedCanvas;
const ctx = preparedCanvas.getContext("2d")!;
const boxShadowRows: string[] = [];
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const [r, g, b, a] = ctx.getImageData(x, y, 1, 1).data;
// Optimization: Omit current if fully transparent.
if (a == 0) {
continue;
}
boxShadowRows.push(
`${pxAsString(x)} ${pxAsString(y)} ${rgbAsString(r, g, b, a)}`
);
}
}
return boxShadowRows.join(",");
};
export const initBoxShadowImage = (): void => {
const fileUpload: HTMLInputElement = document.getElementById(
"fileUpload"
)! as HTMLInputElement;
const canvas: HTMLCanvasElement = document.getElementById(
"canvas"
) as HTMLCanvasElement;
const output: HTMLTextAreaElement = document.getElementById(
"output"
) as HTMLTextAreaElement;
const demo: HTMLDivElement = document.getElementById(
"demo"
) as HTMLDivElement;
const doConversion = (image: HTMLImageElement): void => {
prepareCanvas(canvas, image);
const boxShadowImage = createBoxShadowImageFromCanvas(canvas);
output.textContent = boxShadowImage;
console.time("DRAW");
demo.style.boxShadow = boxShadowImage;
console.timeEnd("DRAW");
};
fileUpload.addEventListener("input", () => {
const imageFile = fileUpload.files?.item(0);
if (imageFile == null) {
throw new TypeError("Could not read image.");
}
let image: HTMLImageElement;
readFileAsDataUrl(imageFile)
.then((imageDataUrl) => {
image = new Image();
image.src = imageDataUrl;
return image.decode();
})
.then(() => {
document.body.appendChild(image);
doConversion(image);
image.remove();
})
.catch(console.error);
});
};
@FelixRilling
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment