Extract background color to continue image

This commit is contained in:
nemunaire 2024-03-18 11:26:01 +01:00
commit 26c282138e
23 changed files with 218 additions and 115 deletions

View file

@ -677,7 +677,7 @@ func createExercice(c *gin.Context) {
}
}
exercice, err := theme.AddExercice(ue.Title, ue.Authors, ue.Image, ue.WIP, ue.URLId, ue.Path, ue.Statement, ue.Overview, ue.Headline, depend, ue.Gain, ue.VideoURI, ue.Resolution, ue.SeeAlso, ue.Finished)
exercice, err := theme.AddExercice(ue.Title, ue.Authors, ue.Image, ue.BackgroundColor, ue.WIP, ue.URLId, ue.Path, ue.Statement, ue.Overview, ue.Headline, depend, ue.Gain, ue.VideoURI, ue.Resolution, ue.SeeAlso, ue.Finished)
if err != nil {
log.Println("Unable to createExercice:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during exercice creation."})

View file

@ -1715,7 +1715,7 @@ angular.module("FICApp")
})
.controller("ThemeController", function($scope, Theme, $routeParams, $location, $rootScope, $http) {
$scope.theme = Theme.get({ themeId: $routeParams.themeId });
$scope.fields = ["name", "urlid", "locked", "authors", "headline", "intro", "image", "partner_txt", "partner_href", "partner_img"];
$scope.fields = ["name", "urlid", "locked", "authors", "headline", "intro", "image", "background_color", "partner_txt", "partner_href", "partner_img"];
$scope.saveTheme = function() {
if (this.theme.id) {
@ -1893,7 +1893,7 @@ angular.module("FICApp")
}
});
$scope.exercices = Exercice.query();
$scope.fields = ["title", "urlid", "authors", "disabled", "statement", "headline", "overview", "finished", "depend", "gain", "coefficient", "videoURI", "image", "resolution", "issue", "issuekind", "wip"];
$scope.fields = ["title", "urlid", "authors", "disabled", "statement", "headline", "overview", "finished", "depend", "gain", "coefficient", "videoURI", "image", "background_color", "resolution", "issue", "issuekind", "wip"];
$scope.inSync = false;
$scope.syncExo = function() {

View file

@ -21,7 +21,7 @@
<div class="form-group row" ng-repeat="field in fields">
<label for="{{ field }}" class="col-sm-1 col-form-label-sm">{{ field | capitalize }}</label>
<div class="col-sm-11">
<input type="text" class="form-control form-control-sm" id="{{ field }}" ng-model="exercice[field]" ng-if="field != 'statement' && field != 'issue' && field != 'issuekind' && field != 'overview' && field != 'resolution' && field != 'finished' && field != 'depend' && field != 'gain' && field != 'coefficient' && field != 'wip' && field != 'disabled'">
<input type="text" class="form-control form-control-sm" id="{{ field }}" ng-model="exercice[field]" ng-if="field != 'statement' && field != 'issue' && field != 'issuekind' && field != 'overview' && field != 'resolution' && field != 'finished' && field != 'depend' && field != 'gain' && field != 'coefficient' && field != 'wip' && field != 'disabled' && field != 'background_color'">
<input type="checkbox" id="{{ field }}" ng-model="exercice[field]" ng-if="field == 'wip' || field == 'disabled'">
<input type="text" class="form-control form-control-sm" id="{{ field }}" ng-model="exercice[field]" ng-if="field == 'gain'" integer>
<input type="text" class="form-control form-control-sm" id="{{ field }}" ng-model="exercice[field]" ng-if="field == 'coefficient'" float>
@ -30,6 +30,7 @@
<option value="">Aucune</option>
</select>
<select class="form-control form-control-sm" id="{{field}}" ng-model="exercice[field]" ng-options="v for v in ['primary', 'secondary', 'success', 'danger', 'warning', 'info', 'light', 'dark']" ng-if="field == 'issuekind'"></select>
<input type="color" class="form-control form-control-sm" id="{{ field }}" ng-model="exercice[field]" ng-if="field == 'background_color'" color>
</div>
</div>
<div class="text-right" ng-show="exercice.id">

View file

@ -12,8 +12,9 @@
<div ng-class="{'form-group': field != 'locked', 'form-check': field == 'locked'}" ng-repeat="field in fields">
<input type="checkbox" class="form-check-input" id="{{ field }}" ng-model="theme[field]" ng-if="field == 'locked'">
<label for="{{ field }}">{{ field | capitalize }}</label>
<input type="text" class="form-control form-control-sm" id="{{ field }}" ng-model="theme[field]" ng-if="field != 'intro' && field != 'locked'">
<input type="text" class="form-control form-control-sm" id="{{ field }}" ng-model="theme[field]" ng-if="field != 'intro' && field != 'locked' && field != 'background_color'">
<textarea class="form-control form-control-sm" id="{{ field }}" ng-model="theme[field]" ng-if="field == 'intro'"></textarea>
<input type="color" class="form-control form-control-sm" id="{{ field }}" ng-model="theme[field]" ng-if="field == 'background_color'" color>
</div>
<div class="text-right" ng-show="theme.id">
<button type="submit" class="btn btn-success"><span class="glyphicon glyphicon-save" aria-hidden="true"></span> Save</button>

View file

@ -397,6 +397,8 @@ func SyncExercice(i Importer, theme *fic.Theme, epath string, dmap *map[int64]*f
e.Image = strings.TrimPrefix(filePath, fic.FilesDir)
e.BackgroundColor, _ = getBackgroundColor(filePath)
// If the theme has no image yet, use the first exercice's image found
theme.Image = e.Image
_, err := theme.Update()

View file

@ -13,6 +13,7 @@ import (
"strings"
"unicode"
"github.com/cenkalti/dominantcolor"
"github.com/gin-gonic/gin"
"github.com/yuin/goldmark"
"go.uber.org/multierr"
@ -86,6 +87,49 @@ func resizePicture(importedPath string, rect image.Rectangle) error {
return nil
}
type SubImager interface {
SubImage(r image.Rectangle) image.Image
}
// getBackgroundColor retrieves the most dominant color in the bottom of the image.
func getBackgroundColor(importedPath string) (uint32, error) {
fl, err := os.Open(strings.TrimSuffix(importedPath, ".jpg") + ".thumb.jpg")
if err != nil {
return 0, err
}
src, _, err := image.Decode(fl)
if err != nil {
fl.Close()
return 0, err
}
bounds := src.Bounds()
// Test if the right and left corner have the same color
bottomLeft := src.(SubImager).SubImage(image.Rect(0, bounds.Dy()-10, 40, bounds.Dy()))
bottomRight := src.(SubImager).SubImage(image.Rect(bounds.Dx()-40, bounds.Dy()-10, bounds.Dx(), bounds.Dy()))
colorLeft := dominantcolor.Find(bottomLeft)
colorRight := dominantcolor.Find(bottomRight)
if uint32(colorLeft.R>>5)<<16+uint32(colorLeft.G>>5)<<8+uint32(colorLeft.B>>5) == uint32(colorRight.R>>5)<<16+uint32(colorRight.G>>5)<<8+uint32(colorRight.B>>5) {
return uint32(colorLeft.R)<<16 + uint32(colorLeft.G)<<8 + uint32(colorLeft.B), nil
}
// Only keep the darkest color of the bottom of the image
bottomFull := src.(SubImager).SubImage(image.Rect(0, bounds.Dy()-5, bounds.Dx(), bounds.Dy()))
colors := dominantcolor.FindN(bottomFull, 4)
color := colors[0]
for _, c := range colors {
if uint32(color.R<<2)+uint32(color.G<<2)+uint32(color.B<<2) > uint32(c.R<<2)+uint32(c.G<<2)+uint32(c.B<<2) {
color = c
}
}
return uint32(color.R)<<16 + uint32(color.G)<<8 + uint32(color.B), nil
}
// getAuthors parses the AUTHORS file.
func getAuthors(i Importer, tname string) ([]string, error) {
if authors, err := GetFileContent(i, path.Join(tname, "AUTHORS.txt")); err != nil {
@ -258,6 +302,7 @@ func SyncThemes(i Importer) (exceptions map[string]*CheckExceptions, errs error)
}
btheme.Image = strings.TrimPrefix(filePath, fic.FilesDir)
btheme.BackgroundColor, _ = getBackgroundColor(filePath)
return nil, nil
}); err != nil {
errs = multierr.Append(errs, NewThemeError(btheme, fmt.Errorf("unable to import heading image: %w", err)))

View file

@ -22,6 +22,9 @@
import { settings } from '$lib/stores/settings.js';
import { themesStore } from '$lib/stores/themes.js';
// Override theme color
document.body.style.backgroundColor = "";
let items = [];
$: {
const tmpitems = [];

View file

@ -12,7 +12,21 @@
let heading_image = "";
let current_authors = "";
let background_color = "#ffffff";
let color_brightness = 0;
$: color_brightness = parseInt(background_color[1], 16) + parseInt(background_color[3], 16) + parseInt(background_color[5], 16);
$: if ($current_theme) {
if ($current_exercice && $current_exercice.background_color) {
background_color = $current_exercice.background_color;
document.body.style.backgroundColor = $current_exercice.background_color;
} else if ($current_theme.background_color) {
background_color = $current_theme.background_color;
document.body.style.backgroundColor = $current_theme.background_color;
} else {
background_color = "#ffffff";
document.body.style.backgroundColor = "";
}
if ($current_exercice && $current_exercice.image) {
heading_image = $current_exercice.image;
} else {
@ -48,13 +62,21 @@
</Container>
{:else}
<div style="background-image: url({heading_image})" class="page-header">
<Container class="text-primary">
<h1 class="display-2">
{#if $current_theme.urlid == "_" && $current_exercice}
<a href="{$current_theme.urlid}">{$current_exercice.title}</a>
{:else}
<a href="{$current_theme.urlid}">{$current_theme.name}</a>
{/if}
<Container class="text-primary py-4">
<h1
class="display-2"
style:text-shadow={color_brightness < 24 ? "0 0 15px rgba(255,255,255,0.95), 0 0 5px rgb(255,255,255)" : "0 0 15px rgba(0,0,0,0.95), 0 0 5px rgb(0,0,0)"}
>
<a
href="{$current_theme.urlid}"
style:color={background_color}
>
{#if $current_theme.urlid == "_" && $current_exercice}
{$current_exercice.title}
{:else}
{$current_theme.name}
{/if}
</a>
</h1>
<h2>
{#if current_authors}
@ -64,29 +86,21 @@
{/if}
</h2>
</Container>
{#if heading_image}
<div class="headerfade"></div>
{:else}
<div style="height: 3rem;"></div>
{/if}
<Container class="pb-4">
<slot></slot>
</Container>
</div>
<Container>
<slot></slot>
</Container>
{/if}
<style>
.page-header {
background-size: cover;
background-position: center;
margin-bottom: -15rem;
}
.page-header h1 {
text-shadow: 0 0 15px rgba(255,255,255,0.95), 0 0 5px rgb(255,255,255)
background-size: 100% auto;
background-position: top center;
background-repeat: no-repeat;
}
.page-header h1, .page-header h1 a {
color: black;
text-decoration: none;
filter: invert(100%) hue-rotate(45deg) brightness(100%);
}
.page-header h2 {
font-size: 100%;
@ -99,16 +113,9 @@
text-decoration: underline;
}
.page-header h1 {
padding-top: 4rem;
text-align: center;
}
.page-header h2 {
padding-bottom: 14rem;
text-align: center;
}
.page-header .headerfade {
background: linear-gradient(transparent 0%, rgb(233,236,239) 100%);
height: 3rem;
}
</style>

View file

@ -37,7 +37,7 @@
/>
</Masonry>
{:else}
<Card class="bg-dark niceborder text-indent mt-2 mb-4">
<Card class="bg-dark niceborder text-indent mt-2" style="--bs-bg-opacity: .9;">
<Row>
<Col lg={6} xl={7}>

View file

@ -51,8 +51,8 @@
</script>
{#if $current_exercice}
<Card class="niceborder text-indent my-3">
<CardBody class="bg-dark">
<Card class="niceborder text-indent my-3 bg-primary" style="--bs-bg-opacity: .9;">
<CardBody class="bg-dark" style="--bs-bg-opacity: .5;">
{#if $current_theme.locked}
<div style="position: absolute; z-index: 0; top: 0; bottom: 0; left: 0; right: 0;" class="d-flex justify-content-center align-items-center">
<div style="transform: rotate(-25deg)">
@ -91,7 +91,7 @@
<CardBody>
<Row>
<Col>
<Row class="level" cols={{xs:2, md:3, xl:4}}>
<Row class="level text-light" cols={{xs:2, md:3, xl:4}}>
<Col>
<div class="level-item">
{#if $settings.discountedFactor > 0 && $my && $my.exercices[$current_exercice.id]}

View file

@ -17,6 +17,9 @@
import { my } from '$lib/stores/my.js';
import { settings } from '$lib/stores/settings.js';
// Override theme color
document.body.style.backgroundColor = "";
</script>
<Container class="my-3">

View file

@ -17,6 +17,9 @@
import FormIssue from '$lib/components/FormIssue.svelte';
// Override theme color
document.body.style.backgroundColor = "";
export let data;
let issue = {};

View file

@ -17,6 +17,9 @@
import CardTheme from '$lib/components/CardTheme.svelte';
let search = "";
// Override theme color
document.body.style.backgroundColor = "";
</script>
<Container fluid class="my-3">

View file

@ -85,6 +85,9 @@
message = "Une erreur est survenue lors de l'inscription de l'équipe. Veuillez réessayer dans quelques instants.";
}
}
// Override theme color
document.body.style.backgroundColor = "";
</script>
<Container class="my-3">

View file

@ -8,6 +8,9 @@
import { challengeInfo } from '$lib/stores/challengeinfo.js';
import { settings } from '$lib/stores/settings.js';
// Override theme color
document.body.style.backgroundColor = "";
</script>
<Container class="my-3">

View file

@ -11,6 +11,9 @@
import { themesStore } from '$lib/stores/themes.js';
// Override theme color
document.body.style.backgroundColor = "";
tick().then(() => {
WordCloud(
document.getElementById('wordcloud'),

View file

@ -33,6 +33,9 @@
exercices = tmp_exercices;
}
// Override theme color
document.body.style.backgroundColor = "";
</script>
<Container class="mt-3">

2
go.mod
View file

@ -31,9 +31,11 @@ require (
github.com/asticode/go-astits v1.8.0 // indirect
github.com/aws/aws-sdk-go v1.38.20 // indirect
github.com/bytedance/sonic v1.9.1 // indirect
github.com/cenkalti/dominantcolor v1.0.2 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/cloudflare/circl v1.3.3 // indirect
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
github.com/disintegration/imaging v1.6.2 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect

3
go.sum
View file

@ -65,6 +65,8 @@ github.com/bytedance/sonic v1.8.0 h1:ea0Xadu+sHlu7x5O3gKhRpQ1IKiMrSiHttPF0ybECuA
github.com/bytedance/sonic v1.8.0/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/cenkalti/dominantcolor v1.0.2 h1:nP1qLG2sD4vu+mGjvEcp3zMaiT7OvcRDtp+wE0YEtfg=
github.com/cenkalti/dominantcolor v1.0.2/go.mod h1:HvN7ziRLPAes3UkUrLDDRADCPTFsKUzZx5ZAQx8KECc=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
@ -78,6 +80,7 @@ github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxG
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=

View file

@ -94,6 +94,7 @@ CREATE TABLE IF NOT EXISTS themes(
headline TEXT NOT NULL,
intro TEXT NOT NULL,
image VARCHAR(255) NOT NULL,
background_color INTEGER UNSIGNED NOT NULL,
authors TEXT NOT NULL,
partner_img VARCHAR(255) NOT NULL,
partner_href VARCHAR(255) NOT NULL,
@ -106,7 +107,7 @@ CREATE TABLE IF NOT EXISTS themes(
CREATE TABLE IF NOT EXISTS teams(
id_team INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
color INTEGER NOT NULL,
color INTEGER UNSIGNED NOT NULL,
active BOOLEAN NOT NULL DEFAULT 1,
external_id TEXT NOT NULL,
password VARCHAR(255) NULL
@ -144,6 +145,7 @@ CREATE TABLE IF NOT EXISTS exercices(
title VARCHAR(255) NOT NULL,
authors TEXT NOT NULL,
image VARCHAR(255) NOT NULL,
background_color INTEGER UNSIGNED NOT NULL,
disabled BOOLEAN NOT NULL DEFAULT 0,
headline TEXT