diff --git a/app/assets/javascripts/wizard/components/image-preview-logo-url.js.es6 b/app/assets/javascripts/wizard/components/image-preview-logo-url.js.es6 new file mode 100644 index 00000000000..9bd065d0c99 --- /dev/null +++ b/app/assets/javascripts/wizard/components/image-preview-logo-url.js.es6 @@ -0,0 +1,76 @@ +import { observes } from 'ember-addons/ember-computed-decorators'; + +import { + createPreviewComponent, + loadImage, + drawHeader, + darkLightDiff +} from 'wizard/lib/preview'; + +export default createPreviewComponent(400, 100, { + image: null, + + @observes('field.value') + imageChanged() { + this.reload(); + }, + + load() { + return loadImage(this.get('field.value')).then(image => { + this.image = image; + }); + }, + + paint(ctx, colors, width, height) { + const headerHeight = height / 2; + + drawHeader(ctx, colors, width, headerHeight); + + const image = this.image; + const headerMargin = headerHeight * 0.2; + + const imageHeight = headerHeight - (headerMargin * 2); + const ratio = imageHeight / image.height; + ctx.drawImage(image, headerMargin, headerMargin, image.width * ratio, imageHeight); + + const categoriesSize = width / 3.8; + const badgeHeight = categoriesSize * 0.25; + + ctx.beginPath(); + ctx.fillStyle = darkLightDiff(colors.primary, colors.secondary, 90, -65); + ctx.rect(headerMargin, headerHeight + headerMargin, categoriesSize, badgeHeight); + ctx.fill(); + + const fontSize = Math.round(badgeHeight * 0.5); + ctx.font = `${fontSize}px 'Arial'`; + ctx.fillStyle = colors.primary; + ctx.fillText("all categories", headerMargin * 1.5, headerHeight + (headerMargin * 1.5) + fontSize); + + ctx.font = "0.9em 'FontAwesome'"; + ctx.fillStyle = colors.primary; + ctx.fillText("\uf0da", categoriesSize - (headerMargin / 4), headerHeight + (headerMargin * 1.6) + fontSize); + + // pills + ctx.beginPath(); + ctx.fillStyle = colors.quaternary; + ctx.rect((headerMargin * 2)+ categoriesSize, headerHeight + headerMargin, categoriesSize * 0.55, badgeHeight); + ctx.fill(); + + ctx.font = `${fontSize}px 'Arial'`; + ctx.fillStyle = colors.secondary; + let x = (headerMargin * 3.0) + categoriesSize; + + ctx.fillText("Latest", x, headerHeight + (headerMargin * 1.5) + fontSize); + + ctx.fillStyle = colors.primary; + x += categoriesSize * 0.6; + ctx.fillText("New", x, headerHeight + (headerMargin * 1.5) + fontSize); + + x += categoriesSize * 0.4; + ctx.fillText("Unread", x, headerHeight + (headerMargin * 1.5) + fontSize); + + x += categoriesSize * 0.6; + ctx.fillText("Top", x, headerHeight + (headerMargin * 1.5) + fontSize); + } + +}); diff --git a/app/assets/javascripts/wizard/components/theme-preview.js.es6 b/app/assets/javascripts/wizard/components/theme-preview.js.es6 index 2e676e296d4..0672eac6387 100644 --- a/app/assets/javascripts/wizard/components/theme-preview.js.es6 +++ b/app/assets/javascripts/wizard/components/theme-preview.js.es6 @@ -1,9 +1,13 @@ -/*eslint no-bitwise:0 */ - import { observes } from 'ember-addons/ember-computed-decorators'; -const WIDTH = 400; -const HEIGHT = 220; +import { + createPreviewComponent, + loadImage, + darkLightDiff, + chooseBrighter, + drawHeader +} from 'wizard/lib/preview'; + const LINE_HEIGHT = 12.0; const LOREM = ` @@ -19,107 +23,30 @@ accumsan sapien, nec feugiat quam. Quisque non risus. placerat lacus vitae, lacinia nisi. Sed metus arcu, iaculis sit amet cursus nec, sodales at eros.`; -function loadImage(src) { - const img = new Image(); - img.src = src; - - return new Ember.RSVP.Promise(resolve => img.onload = () => resolve(img)); -}; - -function parseColor(color) { - const m = color.match(/^#([0-9a-f]{6})$/i); - if (m) { - const c = m[1]; - return [ parseInt(c.substr(0,2),16), parseInt(c.substr(2,2),16), parseInt(c.substr(4,2),16) ]; - } - - return [0, 0, 0]; -} - -function brightness(color) { - return (color[0] * 0.299) + (color[1] * 0.587) + (color[2] * 0.114); -} - -function lighten(color, percent) { - return '#' + - ((0|(1<<8) + color[0] + (256 - color[0]) * percent / 100).toString(16)).substr(1) + - ((0|(1<<8) + color[1] + (256 - color[1]) * percent / 100).toString(16)).substr(1) + - ((0|(1<<8) + color[2] + (256 - color[2]) * percent / 100).toString(16)).substr(1); -} - -function chooseBrighter(primary, secondary) { - const primaryCol = parseColor(primary); - const secondaryCol = parseColor(secondary); - - return brightness(primaryCol) < brightness(secondaryCol) ? secondary : primary; -} - -function darkLightDiff(adjusted, comparison, lightness, darkness) { - const adjustedCol = parseColor(adjusted); - const comparisonCol = parseColor(comparison); - return lighten(adjustedCol, (brightness(adjustedCol) < brightness(comparisonCol)) ? - lightness : darkness); -} - -export default Ember.Component.extend({ - ctx: null, - width: WIDTH, - height: HEIGHT, - loaded: false, +export default createPreviewComponent(400, 220, { logo: null, + avatar: null, - themeId: Ember.computed.alias('step.fieldsById.theme_id.value'), + @observes('step.fieldsById.theme_id.value') + themeChanged() { + this.triggerRepaint(); + }, - didInsertElement() { - this._super(); - const c = this.$('canvas')[0]; - this.ctx = c.getContext("2d"); - - Ember.RSVP.Promise.all([loadImage('/images/wizard/discourse-small.png'), + load() { + return Ember.RSVP.Promise.all([loadImage('/images/wizard/discourse-small.png'), loadImage('/images/wizard/trout.png')]).then(result => { this.logo = result[0]; this.avatar = result[1]; - this.loaded = true; - this.triggerRepaint(); }); }, - @observes('themeId') - triggerRepaint() { - Ember.run.scheduleOnce('afterRender', this, 'repaint'); - }, + paint(ctx, colors, width, height) { + const headerHeight = height * 0.15; - repaint() { - if (!this.loaded) { return; } + drawHeader(ctx, colors, width, headerHeight); - const { ctx } = this; - const headerHeight = HEIGHT * 0.15; - - const themeId = this.get('themeId'); - const choices = this.get('step.fieldsById.theme_id.choices'); - if (!choices) { return; } - const option = choices.findProperty('id', themeId); - - const colors = option.data.colors; - if (!colors) { return; } - - ctx.fillStyle = colors.secondary; - ctx.fillRect(0, 0, WIDTH, HEIGHT); - - // Header area - ctx.save(); - ctx.beginPath(); - ctx.rect(0, 0, WIDTH, headerHeight); - ctx.fillStyle = colors.header_background; - ctx.shadowColor = "rgba(0, 0, 0, 0.25)"; - ctx.shadowBlur = 2; - ctx.shadowOffsetX = 0; - ctx.shadowOffsetY = 2; - ctx.fill(); - ctx.restore(); - - const margin = WIDTH * 0.02; - const avatarSize = HEIGHT * 0.1; + const margin = width * 0.02; + const avatarSize = height * 0.1; // Logo const headerMargin = headerHeight * 0.2; @@ -128,19 +55,19 @@ export default Ember.Component.extend({ ctx.drawImage(this.logo, headerMargin, headerMargin, logoWidth, logoHeight); // Top right menu - ctx.drawImage(this.avatar, WIDTH - avatarSize - headerMargin, headerMargin, avatarSize, avatarSize); + ctx.drawImage(this.avatar, width - avatarSize - headerMargin, headerMargin, avatarSize, avatarSize); ctx.fillStyle = darkLightDiff(colors.primary, colors.secondary, 45, 55); ctx.font = "0.75em FontAwesome"; - ctx.fillText("\uf0c9", WIDTH - (avatarSize * 2) - (headerMargin * 0.5), avatarSize); - ctx.fillText("\uf002", WIDTH - (avatarSize * 3) - (headerMargin * 0.5), avatarSize); + ctx.fillText("\uf0c9", width - (avatarSize * 2) - (headerMargin * 0.5), avatarSize); + ctx.fillText("\uf002", width - (avatarSize * 3) - (headerMargin * 0.5), avatarSize); // Draw a fake topic - ctx.drawImage(this.avatar, margin, headerHeight + (HEIGHT * 0.17), avatarSize, avatarSize); + ctx.drawImage(this.avatar, margin, headerHeight + (height * 0.17), avatarSize, avatarSize); ctx.beginPath(); ctx.fillStyle = colors.primary; ctx.font = "bold 0.75em 'Arial'"; - ctx.fillText("Welcome to Discourse", margin, (HEIGHT * 0.25)); + ctx.fillText("Welcome to Discourse", margin, (height * 0.25)); ctx.font = "0.5em 'Arial'"; @@ -148,52 +75,45 @@ export default Ember.Component.extend({ const lines = LOREM.split("\n"); for (let i=0; i<10; i++) { - line = (HEIGHT * 0.3) + (i * LINE_HEIGHT); + line = (height * 0.3) + (i * LINE_HEIGHT); ctx.fillText(lines[i], margin + avatarSize + margin, line); } // Reply Button ctx.beginPath(); - ctx.rect(WIDTH * 0.57, line + LINE_HEIGHT, WIDTH * 0.1, HEIGHT * 0.07); + ctx.rect(width * 0.57, line + LINE_HEIGHT, width * 0.1, height * 0.07); ctx.fillStyle = colors.tertiary; ctx.fill(); ctx.fillStyle = chooseBrighter(colors.primary, colors.secondary); ctx.font = "8px 'Arial'"; - ctx.fillText("Reply", WIDTH * 0.595, line + (LINE_HEIGHT * 1.8)); + ctx.fillText("Reply", width * 0.595, line + (LINE_HEIGHT * 1.8)); // Icons ctx.font = "0.5em FontAwesome"; ctx.fillStyle = colors.love; - ctx.fillText("\uf004", WIDTH * 0.48, line + (LINE_HEIGHT * 1.8)); + ctx.fillText("\uf004", width * 0.48, line + (LINE_HEIGHT * 1.8)); ctx.fillStyle = darkLightDiff(colors.primary, colors.secondary, 65, 55); - ctx.fillText("\uf040", WIDTH * 0.525, line + (LINE_HEIGHT * 1.8)); + ctx.fillText("\uf040", width * 0.525, line + (LINE_HEIGHT * 1.8)); // Draw Timeline - const timelineX = WIDTH * 0.8; + const timelineX = width * 0.8; ctx.beginPath(); ctx.strokeStyle = colors.tertiary; ctx.lineWidth = 0.5; - ctx.moveTo(timelineX, HEIGHT * 0.3); - ctx.lineTo(timelineX, HEIGHT * 0.6); + ctx.moveTo(timelineX, height * 0.3); + ctx.lineTo(timelineX, height * 0.6); ctx.stroke(); // Timeline ctx.beginPath(); ctx.strokeStyle = colors.tertiary; ctx.lineWidth = 2; - ctx.moveTo(timelineX, HEIGHT * 0.3); - ctx.lineTo(timelineX, HEIGHT * 0.4); + ctx.moveTo(timelineX, height * 0.3); + ctx.lineTo(timelineX, height * 0.4); ctx.stroke(); ctx.font = "Bold 0.5em Arial"; ctx.fillStyle = colors.primary; - ctx.fillText("1 / 20", timelineX + margin, (HEIGHT * 0.3) + (margin * 1.5)); - - // draw border - ctx.beginPath(); - ctx.strokeStyle='rgba(0, 0, 0, 0.2)'; - ctx.rect(0, 0, WIDTH, HEIGHT); - ctx.stroke(); + ctx.fillText("1 / 20", timelineX + margin, (height * 0.3) + (margin * 1.5)); } - }); diff --git a/app/assets/javascripts/wizard/lib/preview.js.es6 b/app/assets/javascripts/wizard/lib/preview.js.es6 new file mode 100644 index 00000000000..f6008e31a88 --- /dev/null +++ b/app/assets/javascripts/wizard/lib/preview.js.es6 @@ -0,0 +1,160 @@ +/*eslint no-bitwise:0 */ + +export function createPreviewComponent(width, height, obj) { + return Ember.Component.extend({ + layoutName: 'components/theme-preview', + width, + height, + ctx: null, + loaded: false, + + didInsertElement() { + this._super(); + const c = this.$('canvas')[0]; + this.ctx = c.getContext("2d"); + this.reload(); + }, + + reload() { + this.load().then(() => { + this.loaded = true; + this.triggerRepaint(); + }); + }, + + triggerRepaint() { + Ember.run.scheduleOnce('afterRender', this, 'repaint'); + }, + + repaint() { + if (!this.loaded) { return false; } + + const colors = this.get('wizard').getCurrentColors(); + if (!colors) { return; } + + const { ctx } = this; + + ctx.fillStyle = colors.secondary; + ctx.fillRect(0, 0, width, height); + + this.paint(ctx, colors, this.width, this.height); + + // draw border + ctx.beginPath(); + ctx.strokeStyle='rgba(0, 0, 0, 0.2)'; + ctx.rect(0, 0, width, height); + ctx.stroke(); + } + }, obj); +} + +export function loadImage(src) { + const img = new Image(); + img.src = src; + + return new Ember.RSVP.Promise(resolve => img.onload = () => resolve(img)); +}; + +export function parseColor(color) { + const m = color.match(/^#([0-9a-f]{6})$/i); + if (m) { + const c = m[1]; + return [ parseInt(c.substr(0,2),16), parseInt(c.substr(2,2),16), parseInt(c.substr(4,2),16) ]; + } + + return [0, 0, 0]; +} + +export function brightness(color) { + return (color[0] * 0.299) + (color[1] * 0.587) + (color[2] * 0.114); +} + +function rgbToHsl(r, g, b) { + r /= 255; + g /= 255; + b /= 255; + let max = Math.max(r, g, b), min = Math.min(r, g, b); + let h, s, l = (max + min) / 2; + + if (max === min) { + h = s = 0; + } else { + const d = max - min; + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + switch(max){ + case r: h = (g - b) / d + (g < b ? 6 : 0); break; + case g: h = (b - r) / d + 2; break; + case b: h = (r - g) / d + 4; break; + } + h /= 6; + } + + return [h, s, l]; +} + +function hue2rgb(p, q, t) { + if (t < 0) { t += 1; } + if (t > 1) { t -= 1; } + if (t < 1/6) { return p + (q - p) * 6 * t; } + if (t < 1/2) { return q; } + if (t < 2/3) { return p + (q - p) * (2/3 - t) * 6; } + return p; +} + +function hslToRgb(h, s, l) { + let r, g, b; + + if (s === 0) { + r = g = b = l; // achromatic + } else { + const q = l < 0.5 ? l * (1 + s) : l + s - l * s; + const p = 2 * l - q; + r = hue2rgb(p, q, h + 1/3); + g = hue2rgb(p, q, h); + b = hue2rgb(p, q, h - 1/3); + } + + return [r * 255, g * 255, b * 255]; +} + +export function lighten(color, percent) { + const hsl = rgbToHsl(color[0], color[1], color[2]); + const scale = percent / 100.0; + const diff = scale > 0 ? 1.0 - hsl[2] : hsl[2]; + + hsl[2] = hsl[2] + diff * scale; + color = hslToRgb(hsl[0], hsl[1], hsl[2]); + + return '#' + + ((0|(1<<8) + color[0]).toString(16)).substr(1) + + ((0|(1<<8) + color[1]).toString(16)).substr(1) + + ((0|(1<<8) + color[2]).toString(16)).substr(1); +} + +export function chooseBrighter(primary, secondary) { + const primaryCol = parseColor(primary); + const secondaryCol = parseColor(secondary); + return brightness(primaryCol) < brightness(secondaryCol) ? secondary : primary; +} + +export function darkLightDiff(adjusted, comparison, lightness, darkness) { + const adjustedCol = parseColor(adjusted); + const comparisonCol = parseColor(comparison); + return lighten(adjustedCol, (brightness(adjustedCol) < brightness(comparisonCol)) ? + lightness : darkness); +} + + +export function drawHeader(ctx, colors, width, height) { + ctx.save(); + ctx.beginPath(); + ctx.rect(0, 0, width, height); + ctx.fillStyle = colors.header_background; + ctx.shadowColor = "rgba(0, 0, 0, 0.25)"; + ctx.shadowBlur = 2; + ctx.shadowOffsetX = 0; + ctx.shadowOffsetY = 2; + ctx.fill(); + ctx.restore(); +} + diff --git a/app/assets/javascripts/wizard/models/wizard.js.es6 b/app/assets/javascripts/wizard/models/wizard.js.es6 index 6b53f545bb4..156a55adc5c 100644 --- a/app/assets/javascripts/wizard/models/wizard.js.es6 +++ b/app/assets/javascripts/wizard/models/wizard.js.es6 @@ -5,7 +5,28 @@ import computed from 'ember-addons/ember-computed-decorators'; const Wizard = Ember.Object.extend({ @computed('steps.length') - totalSteps: length => length + totalSteps: length => length, + + // A bit clunky, but get the current colors from the appropriate step + getCurrentColors() { + const colorStep = this.get('steps').findProperty('id', 'colors'); + if (!colorStep) { return; } + + const themeChoice = colorStep.get('fieldsById.theme_id'); + if (!themeChoice) { return; } + + const themeId = themeChoice.get('value'); + if (!themeId) { return; } + + const choices = themeChoice.get('choices'); + if (!choices) { return; } + + const option = choices.findProperty('id', themeId); + if (!option) { return; } + + return option.data.colors; + } + }); export function findWizard() { diff --git a/app/assets/javascripts/wizard/templates/components/wizard-field-image.hbs b/app/assets/javascripts/wizard/templates/components/wizard-field-image.hbs index 356ce943383..209f344e0f5 100644 --- a/app/assets/javascripts/wizard/templates/components/wizard-field-image.hbs +++ b/app/assets/javascripts/wizard/templates/components/wizard-field-image.hbs @@ -1,5 +1,5 @@ {{#if field.value}} - {{component previewComponent field=field fieldClass=fieldClass}} + {{component previewComponent field=field fieldClass=fieldClass wizard=wizard}} {{/if}}
- {{component inputComponentName field=field step=step fieldClass=fieldClass}} + {{component inputComponentName field=field step=step fieldClass=fieldClass wizard=wizard}}
{{#if field.errorDescription}} diff --git a/app/assets/javascripts/wizard/templates/components/wizard-step.hbs b/app/assets/javascripts/wizard/templates/components/wizard-step.hbs index 82e8bd6d0d7..61f21fe1a97 100644 --- a/app/assets/javascripts/wizard/templates/components/wizard-step.hbs +++ b/app/assets/javascripts/wizard/templates/components/wizard-step.hbs @@ -9,7 +9,7 @@ {{#wizard-step-form step=step}} {{#each step.fields as |field|}} - {{wizard-field field=field step=step}} + {{wizard-field field=field step=step wizard=wizard}} {{/each}} {{/wizard-step-form}}