Skip to content

Commit

Permalink
Add trails, dual-hue, and color-palette effects
Browse files Browse the repository at this point in the history
  • Loading branch information
MikeyBurkman committed Oct 8, 2024
1 parent 2e89fa4 commit 56303d1
Show file tree
Hide file tree
Showing 24 changed files with 233 additions and 38 deletions.
20 changes: 10 additions & 10 deletions docs/asset-manifest.json
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
{
"files": {
"main.js": "/partymoji/static/js/main.213c5e9d.chunk.js",
"main.js.map": "/partymoji/static/js/main.213c5e9d.chunk.js.map",
"main.js": "/partymoji/static/js/main.dab44c5b.chunk.js",
"main.js.map": "/partymoji/static/js/main.dab44c5b.chunk.js.map",
"runtime-main.js": "/partymoji/static/js/runtime-main.39af959e.js",
"runtime-main.js.map": "/partymoji/static/js/runtime-main.39af959e.js.map",
"static/js/2.1ec7ca6a.chunk.js": "/partymoji/static/js/2.1ec7ca6a.chunk.js",
"static/js/2.1ec7ca6a.chunk.js.map": "/partymoji/static/js/2.1ec7ca6a.chunk.js.map",
"static/js/2.881bd7cb.chunk.js": "/partymoji/static/js/2.881bd7cb.chunk.js",
"static/js/2.881bd7cb.chunk.js.map": "/partymoji/static/js/2.881bd7cb.chunk.js.map",
"index.html": "/partymoji/index.html",
"static/js/2.1ec7ca6a.chunk.js.LICENSE.txt": "/partymoji/static/js/2.1ec7ca6a.chunk.js.LICENSE.txt",
"static/js/effect.worker.158c42ad.worker.js": "/partymoji/static/js/effect.worker.158c42ad.worker.js",
"static/js/effect.worker.158c42ad.worker.js.LICENSE.txt": "/partymoji/static/js/effect.worker.158c42ad.worker.js.LICENSE.txt",
"static/js/effect.worker.158c42ad.worker.js.map": "/partymoji/static/js/effect.worker.158c42ad.worker.js.map"
"static/js/2.881bd7cb.chunk.js.LICENSE.txt": "/partymoji/static/js/2.881bd7cb.chunk.js.LICENSE.txt",
"static/js/effect.worker.d8c48e28.worker.js": "/partymoji/static/js/effect.worker.d8c48e28.worker.js",
"static/js/effect.worker.d8c48e28.worker.js.LICENSE.txt": "/partymoji/static/js/effect.worker.d8c48e28.worker.js.LICENSE.txt",
"static/js/effect.worker.d8c48e28.worker.js.map": "/partymoji/static/js/effect.worker.d8c48e28.worker.js.map"
},
"entrypoints": [
"static/js/runtime-main.39af959e.js",
"static/js/2.1ec7ca6a.chunk.js",
"static/js/main.213c5e9d.chunk.js"
"static/js/2.881bd7cb.chunk.js",
"static/js/main.dab44c5b.chunk.js"
]
}
2 changes: 1 addition & 1 deletion docs/index.html
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/partymoji/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="description" content="App for creating animated gifs and emojis"/><link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons"/><script>window.ENV="PROD"</script><script src="https://unpkg.com/@material-ui/core@latest/umd/material-ui.production.min.js"></script><link rel="manifest" href="/partymoji/manifest.json"/><title>Partymoji</title><script>!function(e,a,t,n,g,c,o){e.GoogleAnalyticsObject=g,e.ga=e.ga||function(){(e.ga.q=e.ga.q||[]).push(arguments)},e.ga.l=1*new Date,c=a.createElement(t),o=a.getElementsByTagName(t)[0],c.async=1,c.src="https://www.google-analytics.com/analytics.js",o.parentNode.insertBefore(c,o)}(window,document,"script",0,"ga"),ga("create","UA-XXXXX-Y","auto"),ga("send","pageview")</script><script async src="https://www.googletagmanager.com/gtag/js?id=G-DG40RRJF9R"></script><script>function gtag(){dataLayer.push(arguments)}window.dataLayer=window.dataLayer||[],gtag("js",new Date),gtag("config","G-DG40RRJF9R")</script></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div style="background-color:#effffd" id="root"/><script>!function(e){function r(r){for(var n,i,a=r[0],l=r[1],p=r[2],c=0,s=[];c<a.length;c++)i=a[c],Object.prototype.hasOwnProperty.call(o,i)&&o[i]&&s.push(o[i][0]),o[i]=0;for(n in l)Object.prototype.hasOwnProperty.call(l,n)&&(e[n]=l[n]);for(f&&f(r);s.length;)s.shift()();return u.push.apply(u,p||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,a=1;a<t.length;a++){var l=t[a];0!==o[l]&&(n=!1)}n&&(u.splice(r--,1),e=i(i.s=t[0]))}return e}var n={},o={1:0},u=[];function i(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,i),t.l=!0,t.exports}i.m=e,i.c=n,i.d=function(e,r,t){i.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,r){if(1&r&&(e=i(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(i.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)i.d(t,n,function(r){return e[r]}.bind(null,n));return t},i.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(r,"a",r),r},i.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},i.p="/partymoji/";var a=this.webpackJsonppartymoji=this.webpackJsonppartymoji||[],l=a.push.bind(a);a.push=r,a=a.slice();for(var p=0;p<a.length;p++)r(a[p]);var f=l;t()}([])</script><script src="/partymoji/static/js/2.1ec7ca6a.chunk.js"></script><script src="/partymoji/static/js/main.213c5e9d.chunk.js"></script></body><footer><div style="font-size:.7rem">Built: 09/23/24 01:38:27 PM EDT</div></footer></html>
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/partymoji/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="description" content="App for creating animated gifs and emojis"/><link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons"/><script>window.ENV="PROD"</script><script src="https://unpkg.com/@material-ui/core@latest/umd/material-ui.production.min.js"></script><link rel="manifest" href="/partymoji/manifest.json"/><title>Partymoji</title><script>!function(e,a,t,n,g,c,o){e.GoogleAnalyticsObject=g,e.ga=e.ga||function(){(e.ga.q=e.ga.q||[]).push(arguments)},e.ga.l=1*new Date,c=a.createElement(t),o=a.getElementsByTagName(t)[0],c.async=1,c.src="https://www.google-analytics.com/analytics.js",o.parentNode.insertBefore(c,o)}(window,document,"script",0,"ga"),ga("create","UA-XXXXX-Y","auto"),ga("send","pageview")</script><script async src="https://www.googletagmanager.com/gtag/js?id=G-DG40RRJF9R"></script><script>function gtag(){dataLayer.push(arguments)}window.dataLayer=window.dataLayer||[],gtag("js",new Date),gtag("config","G-DG40RRJF9R")</script></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div style="background-color:#effffd" id="root"/><script>!function(e){function r(r){for(var n,i,a=r[0],l=r[1],p=r[2],c=0,s=[];c<a.length;c++)i=a[c],Object.prototype.hasOwnProperty.call(o,i)&&o[i]&&s.push(o[i][0]),o[i]=0;for(n in l)Object.prototype.hasOwnProperty.call(l,n)&&(e[n]=l[n]);for(f&&f(r);s.length;)s.shift()();return u.push.apply(u,p||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,a=1;a<t.length;a++){var l=t[a];0!==o[l]&&(n=!1)}n&&(u.splice(r--,1),e=i(i.s=t[0]))}return e}var n={},o={1:0},u=[];function i(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,i),t.l=!0,t.exports}i.m=e,i.c=n,i.d=function(e,r,t){i.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,r){if(1&r&&(e=i(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(i.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)i.d(t,n,function(r){return e[r]}.bind(null,n));return t},i.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(r,"a",r),r},i.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},i.p="/partymoji/";var a=this.webpackJsonppartymoji=this.webpackJsonppartymoji||[],l=a.push.bind(a);a.push=r,a=a.slice();for(var p=0;p<a.length;p++)r(a[p]);var f=l;t()}([])</script><script src="/partymoji/static/js/2.881bd7cb.chunk.js"></script><script src="/partymoji/static/js/main.dab44c5b.chunk.js"></script></body><footer><div style="font-size:.7rem">Built: 10/08/24 05:07:53 PM EDT</div></footer></html>
1 change: 0 additions & 1 deletion docs/static/js/2.1ec7ca6a.chunk.js.map

This file was deleted.

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/static/js/2.881bd7cb.chunk.js.map

Large diffs are not rendered by default.

3 changes: 0 additions & 3 deletions docs/static/js/effect.worker.158c42ad.worker.js

This file was deleted.

1 change: 0 additions & 1 deletion docs/static/js/effect.worker.158c42ad.worker.js.map

This file was deleted.

3 changes: 3 additions & 0 deletions docs/static/js/effect.worker.d8c48e28.worker.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/static/js/effect.worker.d8c48e28.worker.js.map

Large diffs are not rendered by default.

2 changes: 0 additions & 2 deletions docs/static/js/main.213c5e9d.chunk.js

This file was deleted.

1 change: 0 additions & 1 deletion docs/static/js/main.213c5e9d.chunk.js.map

This file was deleted.

2 changes: 2 additions & 0 deletions docs/static/js/main.dab44c5b.chunk.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/static/js/main.dab44c5b.chunk.js.map

Large diffs are not rendered by default.

26 changes: 13 additions & 13 deletions src/domain/utils/canvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,17 @@ export const frameToCanvas = ({
return { canvas, ctx };
};

/** Convert a CanvasData into a FrameData */
export const canvasToFrame = (canvasData: CanvasData): FrameData => {
const imageData = canvasData.ctx.getImageData(
0,
0,
canvasData.canvas.width,
canvasData.canvas.height
);
return imageData.data;
};

/**
* Draws a frame onto the canvas.
* This respects transforms applied to the canvas, such as scaling.
Expand All @@ -55,20 +66,9 @@ export const drawImageOnCanvas = ({
ctx.drawImage(frameToCanvas({ dimensions, frame }).canvas, 0, 0);
};

/** Convert a CanvasData into a FrameData */
export const canvasToFrame = (canvasData: CanvasData): FrameData => {
const imageData = canvasData.ctx.getImageData(
0,
0,
canvasData.canvas.width,
canvasData.canvas.height
);
return imageData.data;
};

/**
* Allows you to apply a JS Canvas to an image.
* Use the `preEffect` and `postEffect` functions to maniuplate the image
* Use the `preEffect` and `postEffect` functions to manipulate the image
* both before and after drawing the image to the canvas.
*/
export const applyCanvasFromFrame = ({
Expand Down Expand Up @@ -143,7 +143,7 @@ export const applyFilter = (
sepia,
dropShadow,
}: {
/** # pixels */
/** Number of pixels */
blur?: number;
/** [0, 100) percent */
opacity?: number;
Expand Down
29 changes: 29 additions & 0 deletions src/domain/utils/color.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,13 @@ export const shiftHue = ([r, g, b, a]: Color, amount: number): Color => {
return [newR, newG, newB, a];
};

// Amount: 0 - 100
export const setBrightness = ([r, g, b, a]: Color, amount: number): Color => {
const [h, s] = convert.rgb.hsl([r, g, b]);
const [newR, newG, newB] = convert.hsl.rgb([h, s, amount]);
return [newR, newG, newB, a];
};

/**
* Turn a hue value (0 - 360) into a Color
*/
Expand Down Expand Up @@ -142,3 +149,25 @@ export const colorDiff = (c1: Color, c2: Color): number => {
// 765 = ~ difference between black and white pixels
return Math.sqrt(rComponent + bComponent + gComponent) / 765;
};

/**
* Returns a color between c1 and c2.
* Amount is between 0 and 1.
* An amount of 0 will return c1, and amount of 1 will return c2.
*/
export const linearInterpolation = ({
c1: [r1, g1, b1],
c2: [r2, g2, b2],
amount,
}: {
c1: Color;
c2: Color;
amount: number;
}): Color => {
return [
Math.floor((1 - amount) * r1 + amount * r2),
Math.floor((1 - amount) * g1 + amount * g2),
Math.floor((1 - amount) * b1 + amount * b2),
255,
];
};
2 changes: 1 addition & 1 deletion src/domain/utils/imageImport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ export const readImage = async (
return {
image,
dataUrl,
fps: 1,
fps: 20, // Default FPS
};
};

Expand Down
2 changes: 1 addition & 1 deletion src/effects/background-color.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { buildEffect } from './utils';
export const backgroundColor = buildEffect({
name: 'Background Color',
group: 'Colors',
description: 'Change all transparent pixles to the given color',
description: 'Change all transparent pixels to the given color',
params: [
colorPickerParam({
name: 'Color',
Expand Down
44 changes: 44 additions & 0 deletions src/effects/color-palette.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { range } from 'remeda';
import { colorUtil, imageUtil } from '~/domain/utils';
import { colorPickerParam, variableLengthParam } from '~/params';
import { buildEffect } from './utils';

const NUM_DEFAULT_COLORS = 6;
const DEFAULT_COLORS = range(0, NUM_DEFAULT_COLORS).map((v) =>
colorUtil.setBrightness(
colorUtil.fromHexColor('#98651B'),
(1 - v / NUM_DEFAULT_COLORS) * 100
)
);

export const colorPalette = buildEffect({
name: 'Color Palette',
group: 'Colors',
description: 'Change all colors in the image to a set palette.',
secondaryDescription:
'For instance, the lightest colors in the source image will ' +
'be replaced by the first color, and the darkest colors in the source image will be ' +
'replaced by the last color, etc',
params: [
variableLengthParam({
name: 'Palette',
newParamText: 'New Color',
defaultValue: DEFAULT_COLORS,
createNewParam: () =>
colorPickerParam({
name: 'Color',
defaultValue: DEFAULT_COLORS[0],
}),
}),
] as const,
fn: imageUtil.mapImage(({ coord, getSrcPixel, parameters: [palette] }) => {
const src = getSrcPixel(coord);
if (colorUtil.isTransparent(src)) {
return src;
}

const gray = 255 - colorUtil.getAveragePixelValue(src);
const paletteIndex = Math.floor((gray / 256) * palette.length);
return palette[paletteIndex];
}),
});
2 changes: 1 addition & 1 deletion src/effects/drop-shadow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const dropShadow = buildEffect({
}),
] as const,
fn: ({ image, parameters: [offsetX, offsetY, blurRadius, color] }) =>
imageUtil.mapFrames(image, (frame, frameIndex, frameCount) =>
imageUtil.mapFrames(image, (frame) =>
canvasUtil.applyCanvasFromFrame({
dimensions: image.dimensions,
frame,
Expand Down
31 changes: 31 additions & 0 deletions src/effects/dual-hue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { colorUtil, imageUtil } from '~/domain/utils';
import { colorPickerParam } from '~/params';
import { buildEffect } from './utils';

export const dualHue = buildEffect({
name: 'Dual Hue',
group: 'Colors',
description:
'The colors of the image will transition from one color to another',
params: [
colorPickerParam({
name: 'Color 1',
defaultValue: [255, 0, 0, 1],
}),
colorPickerParam({
name: 'Color 2',
defaultValue: [0, 255, 0, 1],
}),
] as const,
fn: imageUtil.mapImage(
({ coord, getSrcPixel, parameters: [color1, color2] }) => {
const src = getSrcPixel(coord);
if (colorUtil.isTransparent(src)) {
return src;
}

const amount = colorUtil.getAveragePixelValue(src) / 256;
return colorUtil.linearInterpolation({ c1: color1, c2: color2, amount });
}
),
});
6 changes: 6 additions & 0 deletions src/effects/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ import { blur } from './blur';
import { bounce } from './bounce';
import { bounceAnimation } from './bounce-animation';
import { circle } from './circle';
import { colorPalette } from './color-palette';
import { colors } from './colors';
import { colorsBackground } from './colors-background';
import { doubleVision } from './double-vision';
import { dropShadow } from './drop-shadow';
import { dualHue } from './dual-hue';
import { expand } from './expand';
import { fade } from './fade';
import { fill } from './fill';
Expand Down Expand Up @@ -50,6 +52,7 @@ import { slowAnimation } from './slow-animation';
import { spin } from './spin';
import { staticc } from './static';
import { text } from './text';
import { trails } from './trails';
import { transparency } from './transparency';
import { transpose } from './transpose';

Expand All @@ -73,10 +76,12 @@ export const POSSIBLE_EFFECTS = pipe(
bounce,
bounceAnimation,
circle,
colorPalette,
colors,
colorsBackground,
doubleVision,
dropShadow,
dualHue,
expand,
fade,
fill,
Expand Down Expand Up @@ -113,6 +118,7 @@ export const POSSIBLE_EFFECTS = pipe(
spin,
staticc,
text,
trails,
transparency,
transpose,
],
Expand Down
85 changes: 85 additions & 0 deletions src/effects/trails.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { canvasUtil } from '~/domain/utils';
import { sliderParam } from '~/params';
import { buildEffect } from './utils';
import { FrameData } from '~/domain/types';

export const trails = buildEffect({
name: 'Trails',
group: 'Misc',
description: 'Adds a trail of previous animation frames',
requiresAnimation: true,
params: [
sliderParam({
name: 'Number of Trails',
description: 'How many previous frames to draw',
min: 1,
max: 10,
defaultValue: 3,
}),
sliderParam({
name: 'Trail Opacity',
description: 'Controls how transparent each trail is',
min: 1,
max: 100,
defaultValue: 50,
}),
sliderParam({
name: 'Trail Blur',
description: 'Blurs each the trails, can make things look smoother',
defaultValue: 0,
min: 0,
max: 100,
}),
] as const,
fn: ({ image, parameters: [numTrails, trailOpacity, blur] }) => {
const blurCoefficient =
(Math.max(image.dimensions[0], image.dimensions[1]) / 10000) * blur;

/*
// assume numTrails = 3, numFrames = 6
frames[0] = src[4]*25% > src[5]*25% > src[0]
frames[1] = src[5]*25% > src[0]*25% > src[1]
frames[2] = src[0]*25% > src[1]*25% > src[2]
frames[3] = src[1]*25% > src[2]*25% > src[3]
frames[4] = src[2]*25% > src[3]*25% > src[4]
frames[5] = src[3]*25% > src[4]*25% > src[5]
...
*/
const frames: FrameData[] = [];
for (let i = 0; i < image.frames.length; i += 1) {
const canvas = canvasUtil.createCanvas(image.dimensions);
for (let n = numTrails; n > 0; n -= 1) {
const idx = i - n + 1;
const frameIdx = idx >= 0 ? idx : image.frames.length + idx;

const frameToCopy = image.frames[frameIdx];
if (frameToCopy != null) {
canvasUtil.applyFilter(canvas, {
opacity: Math.floor((n / numTrails) * trailOpacity),
blur: Math.floor(n * blurCoefficient),
});
canvasUtil.drawImageOnCanvas({
ctx: canvas.ctx,
dimensions: image.dimensions,
frame: frameToCopy,
});
}
}
canvasUtil.applyFilter(canvas, {
opacity: 100,
blur: 0,
});
canvasUtil.drawImageOnCanvas({
ctx: canvas.ctx,
dimensions: image.dimensions,
frame: image.frames[i],
});
frames.push(canvasUtil.canvasToFrame(canvas));
}

return {
dimensions: image.dimensions,
frames,
};
},
});

0 comments on commit 56303d1

Please sign in to comment.