Skip to content

Commit

Permalink
Add option to draw with colors instead of carving
Browse files Browse the repository at this point in the history
  • Loading branch information
clarkwinkelmann committed Apr 4, 2021
1 parent 7c84e34 commit 6e969a4
Show file tree
Hide file tree
Showing 13 changed files with 218 additions and 53 deletions.
7 changes: 6 additions & 1 deletion extend.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
->css(__DIR__ . '/resources/less/forum.less')
->route('/carving-contest', 'carving-contest', Content\Entries::class),
(new Extend\Frontend('admin'))
->js(__DIR__ . '/js/dist/admin.js'),
->js(__DIR__ . '/js/dist/admin.js')
->css(__DIR__ . '/resources/less/admin.less'),
new Extend\Locales(__DIR__ . '/resources/locale'),

(new Extend\Routes('api'))
Expand Down Expand Up @@ -63,4 +64,8 @@

(new Extend\Filter(Filters\EntryFilterer::class))
->addFilter(Filters\NoOpFilter::class),

(new Extend\Settings())
->serializeToForum('carvingContestColorMode', 'carving-contest.colorMode', 'boolval')
->serializeToForum('carvingContestColors', 'carving-contest.colors'),
];
2 changes: 1 addition & 1 deletion js/dist/admin.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion js/dist/admin.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/dist/forum.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/dist/forum.js.map

Large diffs are not rendered by default.

54 changes: 54 additions & 0 deletions js/src/admin/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,60 @@ app.initializers.add('carving-contest', () => {
}),
]);
})
.registerSetting({
type: 'switch',
setting: 'carving-contest.colorMode',
label: app.translator.trans('clarkwinkelmann-carving-contest.admin.settings.colorMode'),
})
.registerSetting(function () {
const setting = this.setting('carving-contest.colors', 'simple');
const disabled = !this.setting('carving-contest.colorMode')();

return [
m('.Form-group.CarvingContest-Subgroup', [
m('label', [
m('input', {
type: 'radio',
name: 'carving-contest-color',
checked: setting() === 'simple',
onchange: () => setting('simple'),
disabled,
}),
' ',
app.translator.trans('clarkwinkelmann-carving-contest.admin.colors.simple'),
]),
m('label', [
m('input', {
type: 'radio',
name: 'carving-contest-color',
checked: setting() === 'all',
onchange: () => setting('all'),
disabled,
}),
' ',
app.translator.trans('clarkwinkelmann-carving-contest.admin.colors.all'),
]),
m('label', [
m('input', {
type: 'radio',
name: 'carving-contest-color',
checked: setting() !== 'simple' && setting() !== 'all',
onchange: () => setting(''),
disabled,
}),
' ',
app.translator.trans('clarkwinkelmann-carving-contest.admin.colors.custom'),
]),
setting() !== 'simple' && setting() !== 'all' ? [
m('input.FormControl', {
bidi: setting,
disabled,
}),
m('.helpText', app.translator.trans('clarkwinkelmann-carving-contest.admin.colors.custom-help')),
] : null,
]),
];
})
.registerPermission({
icon: 'fas fa-spider',
label: app.translator.trans('clarkwinkelmann-carving-contest.admin.permissions.view'),
Expand Down
1 change: 1 addition & 0 deletions js/src/forum/components/ContestPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ export default class ContestPage extends Page {
key: entry.id(), // Without this, canvas are re-used, causing incorrect images to be shown when one is deleted
}, [
m(PumpkinCanvas, {
mode: app.forum.attribute('carvingContestColorMode') ? 'color' : 'carve',
image: entry.image(),
}),
m('h3.CarvingContestEntry--name', entry.name()),
Expand Down
70 changes: 60 additions & 10 deletions js/src/forum/components/ParticipateModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import app from 'flarum/app';
import Modal from 'flarum/common/components/Modal';
import Button from 'flarum/common/components/Button';
import PumpkinCanvas from './PumpkinCanvas';
import BrushState from '../states/BrushState';

const translationPrefix = 'clarkwinkelmann-carving-contest.forum.modal.';

/* global m */

export default class ParticipateModal extends Modal {
toolShape = 'circle';
toolWidth = 30;
brush = new BrushState();
name = '';
image = '';
disabled = true;
Expand All @@ -34,40 +34,90 @@ export default class ParticipateModal extends Modal {
}
}

colorChoice() {
const colors = app.forum.attribute('carvingContestColors');

if (colors === 'all') {
return m('input', {
type: 'color',
value: this.brush.color,
onchange: event => {
this.brush.color = event.target.value;
},
});
}

let colorOptions;

if (colors === 'simple') {
colorOptions = [
'#f32501', // Red
'#ff8d12', // Orange
'#ffe884', // Yellow
'#94ae3f', // Green
'#084f93', // Blue
'#000000', // Black
];
} else {
colorOptions = app.forum.attribute('carvingContestColors').split(',');
}

return m('div', colorOptions.map(color => m('.CarvingContest-ColorChoice', {
style: {
backgroundColor: color,
},
onclick: () => {
this.brush.color = color;
},
className: this.brush.color === color ? 'selected' : '',
})));
}

colorTools() {
if (!app.forum.attribute('carvingContestColorMode')) {
return null;
}

return m('.CarvingContestTools', [
this.colorChoice(),
]);
}

content() {
return m('.Modal-body', [
m('.Form-group', [
this.colorTools(),
m('.CarvingContestTools', [
Button.component({
disabled: this.toolShape === 'circle',
disabled: this.brush.shape === 'circle',
icon: 'fas fa-circle',
className: 'Button',
onclick: () => {
this.toolShape = 'circle';
this.brush.shape = 'circle';
},
}, app.translator.trans(translationPrefix + 'tools.circle')),
Button.component({
disabled: this.toolShape === 'square',
disabled: this.brush.shape === 'square',
icon: 'fas fa-square',
className: 'Button',
onclick: () => {
this.toolShape = 'square';
this.brush.shape = 'square';
},
}, app.translator.trans(translationPrefix + 'tools.square')),
m('input', {
type: 'range',
step: 2,
min: 10,
max: 50,
value: this.toolWidth,
value: this.brush.width,
onchange: event => {
this.toolWidth = parseInt(event.target.value);
this.brush.width = parseInt(event.target.value);
},
}),
]),
m(PumpkinCanvas, {
toolShape: this.toolShape,
toolWidth: this.toolWidth,
mode: app.forum.attribute('carvingContestColorMode') ? 'color' : 'carve',
brush: this.brush,
image: this.image,
onchange: value => {
this.image = value;
Expand Down
93 changes: 55 additions & 38 deletions js/src/forum/components/PumpkinCanvas.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@ const IMAGE_HEIGHT = 426;

export default class PumpkinCanvas {
oninit(vnode) {
this.mode = vnode.attrs.mode;
this.brush = vnode.attrs.brush;

this.previewContext = null;

const imageSourceCanvas = document.createElement('canvas');
imageSourceCanvas.width = IMAGE_WIDTH;
imageSourceCanvas.height = IMAGE_HEIGHT;
this.imageSourceContext = imageSourceCanvas.getContext('2d');
this.imageSourceCanvas = document.createElement('canvas');
this.imageSourceCanvas.width = IMAGE_WIDTH;
this.imageSourceCanvas.height = IMAGE_HEIGHT;
this.imageSourceContext = this.imageSourceCanvas.getContext('2d');
const image = new Image();
image.src = app.forum.attribute('baseUrl') + '/assets/extensions/clarkwinkelmann-carving-contest/pumpkin.jpg';
image.onload = () => {
Expand All @@ -21,10 +24,10 @@ export default class PumpkinCanvas {
this.updatePreview();
};

const drawCanvas = document.createElement('canvas');
drawCanvas.width = IMAGE_WIDTH;
drawCanvas.height = IMAGE_HEIGHT;
this.drawContext = drawCanvas.getContext('2d');
this.drawCanvas = document.createElement('canvas');
this.drawCanvas.width = IMAGE_WIDTH;
this.drawCanvas.height = IMAGE_HEIGHT;
this.drawContext = this.drawCanvas.getContext('2d');

const startingImage = vnode.attrs.image;
if (startingImage) {
Expand Down Expand Up @@ -68,10 +71,7 @@ export default class PumpkinCanvas {
document.removeEventListener('mouseup', this.onmouseup);
}

view(vnode) {
this.toolWidth = vnode.attrs.toolWidth;
this.toolShape = vnode.attrs.toolShape;

view() {
return m('.CarvingContestPumpkin', m('canvas', {
width: IMAGE_WIDTH,
height: IMAGE_HEIGHT,
Expand All @@ -83,8 +83,12 @@ export default class PumpkinCanvas {
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;

if (this.drawEnabled) {
this.drawContext.fillStyle = '#000';
if (this.drawEnabled && this.brush) {
if (this.mode === 'color') {
this.drawContext.fillStyle = this.brush.color;
} else {
this.drawContext.fillStyle = '#000';
}
this.drawWithTool(this.drawContext, x, y, true);

vnode.attrs.onchange(this.drawContext.canvas.toDataURL('image/png'));
Expand All @@ -97,46 +101,59 @@ export default class PumpkinCanvas {
}

drawWithTool(context, x, y, fill = false) {
const width = this.toolWidth;
if (!this.brush) {
return;
}

switch (this.toolShape) {
const width = this.brush.width;

context.beginPath();

switch (this.brush.shape) {
case 'circle':
context.beginPath();
context.arc(x, y, width / 2, 0, 2 * Math.PI);

if (fill) {
context.fill();
} else {
context.stroke();
}
break;
case 'square':
context.fillRect(x - (width / 2), y - (width / 2), width, width);
context.rect(x - (width / 2), y - (width / 2), width, width);
break;
}

if (fill) {
context.fill();
} else {
context.stroke();
}
}

updatePreview(toolPosition = null) {
if (!this.previewContext) {
return;
}

const imageSourceData = this.imageSourceContext.getImageData(0, 0, IMAGE_WIDTH, IMAGE_HEIGHT);
const drawData = this.drawContext.getImageData(0, 0, IMAGE_WIDTH, IMAGE_HEIGHT);

for (let i = 0; i < imageSourceData.data.length; i += 4) {
// If the pixel in that area has an alpha value greater than 0, we create a hole in the image data
// Returning 0 for every index will give rgba(0,0,0,0)
if (drawData.data[i + 3] > 0) {
imageSourceData.data[i] = 0;
imageSourceData.data[i + 1] = 0;
imageSourceData.data[i + 2] = 0;
imageSourceData.data[i + 3] = 0;
this.previewContext.clearRect(0, 0, IMAGE_WIDTH, IMAGE_HEIGHT);

if (this.mode === 'color') {
// In paint mode, we draw the two images on top of another
this.previewContext.drawImage(this.imageSourceCanvas, 0, 0);
this.previewContext.drawImage(this.drawCanvas, 0, 0);
} else {
// In carve mode, we subtract the drawing from the source
const imageSourceData = this.imageSourceContext.getImageData(0, 0, IMAGE_WIDTH, IMAGE_HEIGHT);
const drawData = this.drawContext.getImageData(0, 0, IMAGE_WIDTH, IMAGE_HEIGHT);

for (let i = 0; i < imageSourceData.data.length; i += 4) {
// If the pixel in that area has an alpha value greater than 0, we create a hole in the image data
// Returning 0 for every index will give rgba(0,0,0,0)
if (drawData.data[i + 3] > 0) {
imageSourceData.data[i] = 0;
imageSourceData.data[i + 1] = 0;
imageSourceData.data[i + 2] = 0;
imageSourceData.data[i + 3] = 0;
}
}
}

this.previewContext.clearRect(0, 0, IMAGE_WIDTH, IMAGE_HEIGHT);
this.previewContext.putImageData(imageSourceData, 0, 0);
this.previewContext.putImageData(imageSourceData, 0, 0);
}

if (toolPosition) {
this.previewContext.strokeStyle = 'rgba(0,0,0,0.5)';
Expand Down
5 changes: 5 additions & 0 deletions js/src/forum/states/BrushState.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default class BrushState {
color = null;
shape = 'circle';
width = 30;
}
3 changes: 3 additions & 0 deletions resources/less/admin.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.CarvingContest-Subgroup {
padding-left: 50px;
}
23 changes: 23 additions & 0 deletions resources/less/forum.less
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,32 @@
text-decoration: underline;
}
}

.Avatar {
.Avatar--size(32px);
vertical-align: middle;
margin-right: 5px;
}
}

.CarvingContest-ColorChoice {
display: inline-block;
width: 24px;
height: 24px;
border-radius: 2px;
cursor: pointer;
border: 2px solid white;
box-shadow: 0 0 2px rgba(0, 0, 0, 0.5);

&:not(:first-child) {
margin-left: 5px;
}

&:hover {
border-color: #ccc;
}

&.selected {
border-color: red;
}
}
Loading

0 comments on commit 6e969a4

Please sign in to comment.