Alistair Shepherd2026-03-25T00:00:00Zhttps://www.alistairshepherd.ukFixes for Koken Problems2020-09-17T00:00:00Zhttps://www.alistairshepherd.uk/writing/fixes-for-koken-problems/[Koken](http://koken.me/) was a self-hosted Content Management System built for photographers, offering off-the-shelf features like Adobe Lightroom integration, EXIF handling, album categorisation and much more.
5 years ago, if you were looking for a free photo management website you could host yourself, Koken was a great option. I started with Koken for a photography client in 2014, and decided upon it based on the great documentation and very easy theme development.
Note: I would highly suggest against anyone setting up a new Koken site now. I'm not even sure if it's possible! This is for people maintaining their existing sites.
History of Koken
Koken's public beta was released in Early 2013, and got fairly popular pretty quick. It's creator Todd Dominey nailed what photographers wanted and needed from a self-hosted CMS. Although it became stable and fairly well-supported, it never made it to 1.0.
Fast forward a couple years and in 2015 Koken is bought by NetObjects, a software company which in the 90s had success with a desktop site-builder.
NetObjects continued to update Koken for around 2 years (although focusing on premium functionality), with the last release of v0.22.24 in August 2017. As far as I am aware, there has been no further development of Koken since then. The help centre and social media was active for about another year before they also had no further updates.
As of 2020 Koken still functions, but has some major issues. The store and documentation went offline sometime around the end of 2019 which obviously makes development harder and prevents the easy installation of themes, plugins, and even causes issues with logins on some older versions.
There was a Community Koken Forum called Koken Community, but as Koken died it went not long after, understandably given the lack of any official support.
My relationship with Koken
Every few weeks I get a message on twitter or an email from someone asking for help with their Koken site. This is because I have the 'honour' of being the last tweet on Koken's twitter @koken. In 2017 I developed a theme for Koken for the previously mentioned client called Monolith. Over the end of 2017 and early 2018 I refined this and released it onto GitHub open-sourced under a GPL-v3.0 license. At this point I was unaware of the problems going on with Koken and perhaps had I known that client would be on a different platform.
Since 2017 I have been maintaining a Koken site, and have encountered a few problems that I am consistently asked about. These are my fixes for them, I hope they help!
Note these are all made on v0.22.24 and may differ for previous versions.
"Cannot connect to the API" on login, PHP 7+
When using Koken with PHP 7+ (confirmed on PHP 7.3 and 7.4) and visiting /admin/, you may get a red error box appear with "Cannot connect to the API" without any further error message. Usually this appears with a database error etc, but this is code related. To fix it:
In your installation, find /app/database/DB_Driver.php and take a look at line 1018 and you should see something like this:
else
{
$args = (func_num_args() > 1) ? array_splice(func_get_args(), 1) : null;
if (is_null($args))
{
return call_user_func($function);
}
else
{
return call_user_func_array($function, $args);
}
}
Now, replace line 1028 (the $args declaration) with the following two lines:
$func_args = func_get_args();
$args = (func_num_args() > 1) ? array_splice($func_args, 1) : null;
The code should now look like:
else
{
$func_args = func_get_args();
$args = (func_num_args() > 1) ? array_splice($func_args, 1) : null;
if (is_null($args))
{
return call_user_func($function);
}
else
{
return call_user_func_array($function, $args);
}
}
Images not loading, PHP 7+
You may not notice this immediately if you have images cached, but if you cleared the cache or uploaded a new image you may find it is not rendered. This can also occur in PHP 7+ (confirmed with 7.3 and 7.4) and is another easy fix.
Find /i.php in the root of your installation. On lines 13 and 14 there is the following:
require $root . '/app/koken/Shutter/Shutter.php';
require $root . '/app/koken/Utils/KokenAPI.php';
Replace those lines with:
require_once $root . '/app/koken/Shutter/Shutter.php';
require_once $root . '/app/koken/Utils/KokenAPI.php';
You also need to open /app/koken/Shutter/Shutter.php and on line 274 replace the following:
include dirname(__DIR__) . '/Utils/KokenAPI.php';
with:
include_once dirname(__DIR__) . '/Utils/KokenAPI.php';
Forgotten Password
First try to enter a wrong password and click the "Forgot Password" link that appears in the bottom right. This is the easiest way on more recent version of Koken
Unfortunately however it seems that previous versions relied upon store.koken.me in order to offer forgotten password functionality. We can still reset the password, but it's a bit more manual.
You need access to your koken database for this, whether than be through phpmyadmin, another database management tool, or mysql on the command line. I won't bore with the exact commands/clicks required for each, just the general process.
- Gain access to the database. If you don't know the login details, you can find them in
/storage/configuration/database.php; - Go to the
koken_userstable. This should have only one entry, your user with associated email etc; - Copy the
internal_idfor the user; - Then go to " http://yourwebsite.com/api.php?/users/reset_password/" with the id at the end of the URL;
- Visiting that link should send the email attached to your account a new password
Conclusion
If you haven't gathered from the rest of this post, unfortunately Koken is dead. I will continue to maintain a Koken site as my client cannot afford the cost of a rebuild and relies upon the Lightroom integration, something I haven't seen anywhere else. I would however say you shouldn't be setting up any new Koken websites, and if you still have one you should be seriously looking at alternatives.
There is a change.org petition calling on NetObjects to open-source Koken but I am not hopeful. They are a commercial software company, and most software companies will cling to thier code to the end.
A couple of people have asked me about alternatives now, so I've included a list below. Unfortunately there's nothing quite like it Koken but hopefully one might fill your needs
If you have any further issues with Koken then feel free to leave a comment and I'd be happy to help.
Koken Alternatives
Custom-built website
As a freelance developer I am more or less obligated to suggest that if you have the money, the best solution you will get will be through a web developer or development agency. This can be the design you want and function exactly as you need. If you're interested in my services as a developer or want some throughts on what you need, feel free to get in touch.
Hosted Photography Solutions
There are hosted photography portfolio services out there including:
I haven't used any of these platforms but I have heard they are reliable and easy-to-use and not too expensive.
Hosted Websites
For a bit more work you could use a hosted website builder like Squarespace, Wix, or WordPress.com. This will give you a bit more flexibility than a platform designed to fill a niche.
WordPress
As much as WordPress can have a bad reputation, if you consider themes and plugins carefully then you can get a great looking and performing WordPress site for photography.
It's worth looking for themes that are designed for photography so functionality like EXIF data, lightboxes, and copy protection is included. I don't have any examples but there are several free ones on the official theme directory and many options in commercial theme directories like ThemeForest.
For more Technical...
If you're fairly technically minded, there are also some great programs designed to generate a static website from your content and once you get used to them are really quick and simple.
One I've come across is called "Prosopopee" ( github.com/Psycojoker/prosopopee/) and is designed for photography websites and features everything you'd need. It's a bit more involved to publish content as it's done with text files rather than a GUI or Lightroom integration, and you'll probably initially need a developer to make it look how you'd like but that wouldn't be too hard to come across.
Not focused on Photography, but again for the more technical 11ty ( 11ty.dev) is a static site generator that could absolutely work as a brilliant image gallery. You'd also probably need a developer to get the initial site going and perhaps integrate it with a headless CMS, but with a bit of extra work you can get a system that's a lot more flexible and resilient.
If I were to set up a new site for a photographer, this is probably the direction I'd go down. I've been burned by Koken dying in only a few years, a static site and static site generator will be around a lot longer than that.
If you have any comments or feedback on this article, let me know! I'd love to hear your thoughts, go ahead and send me an email at alistair@accudio.com or contact me on Mastodon.
The post Fixes for Koken Problems appeared first on alistairshepherd.uk.
]]> Making a Parallax SVG Landscape - new site part 12021-01-29T00:00:00Zhttps://www.alistairshepherd.uk/writing/parallax-svg-landscape-1/I've finally finished my new website, and I am really happy with it. I started the site in July 2020 and I launched it in mid January 2021.
It's a big change from Nuxt, Vue and Webpack to doing pretty much everything myself with 11ty (Eleventy) and gulp—I love it. More on that in a future post however, today is about the star of the show—the parallax landscape you see at the top of the page.
If you're the type who wants to dive straight into the code, here's a CodePen - go and have a play!
For those still with me, let's go through it.
Note: I'm writing JavaScript in ES6 and CSS in SCSS. I compile my code anyway so this makes it easier for me to work with.
Inspiration
If you recognise the art-style, it's super inspired by the game Firewatch. Firewatch is a 'walking simulator' game that came out in 2016 and people loved its art style. Featuring a bright, layered landscape it inspired many, myself included. For several years the wallpaper of my phone changed between these wallpapers based on time and weather.
When I was planning my new site, I decided to centre it on this art style. I wanted it to feel interactive, and parallax felt like a natural way to do that.
The Markup
My wonderful sister Becci Shepherd produced the landscape, and sent me a raster PNG for each layer. Although I experimented with masking, it's browser support isn't quite there. SVGs were the obvious choice.
To convert to vector I used Vector Magic Desktop Edition. It does a brilliant job of anything you throw at it, and is the best raster-to-vector converter I've found.
I tidied up the paths in a graphics program; exported it to SVG; tidied up the markup and optimised with SVGOMG. This left me with a decent sized SVG for each layer.
Try ensure the viewbox is identical as it will make sizing much easier.
Now in HTML, we need to stack them:
<div class="landscape" role="img" aria-label="This is equivalent to an img alt attribute.">
<div class="landscape__layer">
<div class="landscape__image">
<svg viewBox="0 0 4000 1000" xmlns="http://www.w3.org/2000/svg">...</svg>
</div>
</div>
<div class="landscape__layer">
<div class="landscape__image">
<svg viewBox="0 0 4000 1000" xmlns="http://www.w3.org/2000/svg">...</svg>
</div>
</div>
<div class="landscape__layer">
<div class="landscape__image">
<svg viewBox="0 0 4000 1000" xmlns="http://www.w3.org/2000/svg">...</svg>
</div>
</div>
... and so on.
</div>
Remember accessibility! Despite being a whole bunch of markup, this is really a fancy image. We userole="img" andaria-label to make it accessible.
I didn't have the two wrapping divs at first, but realised that wrappers for each layer allowed me to use flexbox. This made positioning the SVGs easier:
// wrapping landscape
.landscape {
background: var(--c1);
height: 75vh;
overflow: hidden;
position: relative;
// make each layer fill parent
.landscape__layer {
height: 100%;
left: 0;
position: absolute;
top: 0;
width: 100%;
}
// svg wrapper
.landscape__image {
// position at bottom of element in center
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
// set sizes that work for my image
max-height: 100%;
max-width: 300%;
min-width: 100%;
width: 2500px;
// use flexbox to center SVG elements
display: flex;
flex-direction: column;
}
// basic styling for SVG element
.landscape__image svg {
display: block;
height: auto;
max-width: 100%;
}
We now have a static landscape and are set up to make it more dynamic!
Parallax Animation
Perspective and translateZ
There are two popular methods to implement parallax on the web. The more performant implementation is a CSS-only solution using the perspective CSS property with translateZ(). This is what browser vendors suggest, as it allows the browser to render changes with the GPU. This makes it super quick and smooth and is how I tried to implement it for weeks.
Google Developer docs have a good example of this method.
Although it's great for simple implementations—I found that in my case it was unreliable. This was because:
- Browser implementations vary. Chrome handles a deep 3D perspective easily, but Firefox interprets the spec differently. This meant I had to apply
transform-style: preserve-3don every element between my scroll element and my layers. - Firefox on Android handles 3D transformations inconsistently with deep DOM trees, making it hard to adapt to my structure.
- Mobile Safari on iOS has quirks, and the current solution is to use a trick which 'reverses' the parallax direction. Not an option in my case as that would break the effect!
I spent about two weeks trying to get this working before giving up and going for method two.
JavaScript parallax
JS-based parallax has had a bad rep, as a few popular libraries weren't very performant or accessible. Their size was to deal with browser inconsistencies, but with modern CSS and JS we can do it ourselves without much work.
With CSS custom properties and calc() we can come up with a light and neat implementation ourselves. In JavaScript we use window.requestAnimationFrame and if the scroll position has changed we set it to a custom property.
// constant elements: your main scrolling element; html element
const scrollEl = document.documentElement
const root = document.documentElement
let scrollPos
// update css property on scroll
function animation() {
// check the scroll position has changed
if (scrollPos !== scrollEl.scrollTop) {
// reset the seen scroll position
scrollPos = scrollEl.scrollTop
// update css property --scrollPos with scroll position in pixels
root.style.setProperty('--scrollPos', scrollPos + 'px')
}
// call animation again on next animation frame
window.requestAnimationFrame(animation)
}
// start animation on next animation frame
window.requestAnimationFrame(animation)
That's it. That's all the JavaScript we need. As someone who loves CSS it feels great knowing that we can keep the JS simple and use CSS to implement this descriptively.
The real action is happening in the CSS, this is what we need to add to our previous styles:
.landscape__layer {
// parallax
transform: translateY(calc(var(--scrollPos, 0) * var(--offset, 0)));
@media (prefers-reduced-motion: reduce) {
transform: translateY(0);
}
}
The key line is the first transform and it's custom properties. What we are doing is translating the layer down a certain amount based on the scroll position.
We use a prefers-reduced-motion media query to remove the parallax effect for those who might get motion-sick or prefer less movement in their browsing.
The --offset property is a value that would be between 0 and 1, and changes how much that layer scrolls. Let's look at what happens when we vary that property and scroll down by 100px:
--offset: 0— the element isn't translated and scrolls as normal;--offset: 0.5— the element will be translated down by50px. This makes it look like it's moved50px;--offset: 1— the element is translated down100px, it's in the same place it used to be. This makes it look like it's not moving with scroll;
The --offset property is the key to our parallax system. If each layer has a different value it will scroll at a different speed from the other layers. We can manually set how much each layer will scroll so it looks natural.
The way we apply this to our layers is using the style property. This way we can avoid adding any more CSS, no matter how many layers we have. We set the front layer to 0 so it scrolls with the content, and increase it with each layer. This is what worked for my image:
<div class="landscape" role="img" aria-label="This is equivalent to an img alt attribute.">
<div class="landscape__layer" style="--offset:0.96">...</div>
<div class="landscape__layer" style="--offset:0.92">...</div>
<div class="landscape__layer" style="--offset:0.9">...</div>
<div class="landscape__layer" style="--offset:0.86">...</div>
<div class="landscape__layer" style="--offset:0.83">...</div>
<div class="landscape__layer" style="--offset:0.8">...</div>
<div class="landscape__layer" style="--offset:0.75">...</div>
<div class="landscape__layer" style="--offset:0.4">...</div>
<div class="landscape__layer" style="--offset:0.2">...</div>
<div class="landscape__layer" style="--offset:0">...</div>
</div>
Notice the big gap between 0.4 and 0.75. If you look at the landscape structure, the loch is a lot further away than the trees. We produce the same effect by making the offset a lot further away from 0.
Result
And here we have our final parallax landscape!
Thank you for reading! Next up we're going to take this landscape and add colour schemes—including one that matches the visitors local time!
The post Making a Parallax SVG Landscape - new site part 1 appeared first on alistairshepherd.uk.
]]> SVG Landscape with live colour theming - new site part 22021-02-17T00:00:00Zhttps://www.alistairshepherd.uk/writing/parallax-svg-landscape-2/As promised, now we're going to dig into the colour-changing effects of my new website. If you haven't seen it, check out my previous post about the SVG parallax effect in the landscape.
If you haven't tried it yet, visit my website and click the "paint bucket" icon in the top-right of my website to see the theme picker. Here you can change the colour scheme of the website.
There are four 'static' colour schemes of 'Sunrise', 'Day', 'Sunset' and 'Night'. These set the colours to a specific palette.
I implemented two special 'dynamic' colour schemes, the default of 'Live' and 'Cycle'. Live sets the colour scheme of the website to roughly match your local time, whilst Cycle is a 60 second loop animating through the four static schemes above.
The main point of this post is the colour changing functionality, but I'll briefly mention the 'Sun' animation too.
If you want straight at the code, enjoy! 👋
Note: This post is more technical and less visual than my previous one. There aren't many demos, and it's mostly code snippets from here on. You've been warned!
Background
I have wanted to implement a 'live' functionality in my personal website for a few years. Something that makes my site feel more current and that evolves with the day excited me.
My first attempt at this was in my previous site, where I had a background video of a stream on the Isle of Skye. This was a simple 30s loop, but what I wanted was a 24-hour video that would be synced up with your local time. I liked this idea, but it was impractical thanks to the difficulty in getting 24 hours of consistent footage. It also turned out to be a pretty major technical challenge, I had no experience of streaming video and HLS and DASH weren't widely supported.
When I came up with the idea of the SVG landscape, this seemed like a perfect accompaniment. I could make the time in the 'scene' match up with your local time and demonstrate that through the colours and sun.
Initially I implemented a prototype of this with anime.js—a great JS animation library. When I boiled down the essential elements however, the problem was a lot simpler than I thought. There's more JavaScript here than my previous post but stick with me!
Static Custom Properties
We are starting from the final CodePen in my previous post. First let us set up our colours in custom properties:
As we are going to be using JavaScript to 'enhance' this with the colours of our animation, we're starting with greys that roughly match the tone of our colours.
This helps us in a couple different situations:
- If the visitor has JS disabled or our JS doesn't load, we can be sure the colour contrast is sufficient and the site accessible.
- For performance reasons, we don't want to include our JS blocking the render in the
<head>. That means that for a brief period our fallback colours might be displayed before the JS kicks in. By choosing neutral greys it looks more natural than going from one colour to another—like the saturation is turned up from 0.
Colour Config
So we can access them with JS later, I'm configuring my colours in the JS:
const config = {
states: [\
{\
at: 0,\
name: 'night',\
colours: {\
c0: '#7da5d5',\
c1: '#0c4e8f',\
c2: '#00101f'\
}\
},\
{\
at: 6,\
name: 'sunrise',\
colours: {\
c0: '#fed4d5',\
c1: '#a496c4',\
c2: '#2e2c3f'\
}\
},\
{\
at: 12,\
name: 'day',\
colours: {\
c0: '#ffe2a6',\
c1: '#fc813a',\
c2: '#2f1121'\
}\
},\
{\
at: 18,\
name: 'sunset',\
colours: {\
c0: '#ffad39',\
c1: '#e17b17',\
c2: '#1e0000'\
}\
}\
]
}
We'll add to this later, and the at property will become more clear with more code below. We are defining an array of different themes, giving each a name so we can look them up later, and defining our colour palette.
My website has 10 unique colours, I have reduced it to 3 in code snippets for simplicity. If you're interested in all 10 have a look at the CodePens!
Animating Custom Properties
In CSS we have the animation and transition properties. These help us animate between two values without needing JS. We should be able to use that to animate our custom properties right? Unfortunately, not right.
As great as custom properties are, at the moment they have limits. One of those limits is in animation or transitions. At the moment custom properties are strings, so the browser transition engine can't know how to interpolate between two values when they change.
This is one of the things that the Houdini Project is designed to solve, but it is currently Blink-only so that's not well-supported enough for us at the moment. The idea is you specify exactly the type of value a property represents (eg, colour) and the browser can handle interpolating it.
The Animation
I found it difficult to tutorial-ise the animation JS so what I'm going to do is include my commented code. Feel free to go back to the CodePen above and have a dig around yourself, or get in touch if you have any questions!
// Configuration of colours and animation states
const config = {
// sets the setInterval interval and the progress function for each animation mode
anims: {
live: {
// A high interval as live changes very infrequently.
interval: 60000,
getProgress: now => {
// Current seconds elapsed this day, divided by number of seconds in the day
const time = (now.getHours() * 3600) + (now.getMinutes() * 60) + now.getSeconds()
return time / 86400
}
},
cycle: {
// A low interval as cycle changes in milliseconds.
interval: 50,
getProgress: now => {
// Current milliseconss elapsed this minute, divided by number of milliseconds in a minute
const time = (now.getSeconds() * 1000) + now.getMilliseconds()
return time / 60000
}
}
},
// States with 'at' specifying the time in hours the state should be.
// 'name' allows referring to it when we add themes later.
// 'colours' is object with key as custom property name and value as colour.
states: [\
{\
at: 0,\
name: 'night',\
colours: {\
c0: '#7da5d5',\
c1: '#0c4e8f',\
c2: '#00101f'\
}\
},\
{\
at: 6,\
name: 'sunrise',\
colours: {\
c0: '#fed4d5',\
c1: '#a496c4',\
c2: '#2e2c3f'\
}\
},\
{\
at: 12,\
name: 'day',\
colours: {\
c0: '#ffe2a6',\
c1: '#fc813a',\
c2: '#2f1121'\
}\
},\
{\
at: 18,\
name: 'sunset',\
colours: {\
c0: '#ffad39',\
c1: '#e17b17',\
c2: '#1e0000'\
}\
}\
]
}
const root = document.documentElement
// This changes the interval and progress calculation between
// our dynamic animations 'live' and 'cycle'.
let animMode = 'live'
// Add first element of states to end so we have a seamless loop:
// night > sunrise > day > sunset > night
config.states.push({
...config.states[0],
name: 'end',
at: 24
})
// Declaring our animation loop in a variable allows us to end it when needed.
let animation
function startAnim() {
// Run our update loop immediately after starting.
updateAnim()
// setInterval runs our update loop with a predetermined interval
// based on the animation mode we are using.
animation = setInterval(updateAnim, config.anims[animMode].interval)
}
// If we need to end the animation, this function will stop it
// running again using clearInterval
function endAnim() {
clearInterval(animation)
}
// This runs every update cycle, getting the progress, calculating
// the right colours and applying them to the root element
function updateAnim() {
// Get the progress through the animation. getProgress returns a number between 0 and 1.
// To simplify working with time, we multiply this by 24 to get progress through the day.
const progress = getProgress() * 24
// Find the next 'state' we are transitioning to based on the 'at' property.
// The 'at' property sets at what hour that state should be at.
const nextIndex = config.states.findIndex(frame => {
return frame.at !== 0 && progress < frame.at
})
// The previous 'state' is the one before the next one, so we remove 1.
const lastIndex = nextIndex - 1
// Get the onjects for the last and next states
const lastState = config.states[lastIndex]
const nextState = config.states[nextIndex]
// Calculate the difference between the 'at' values of the previous and last states,
// so we can get our progress between them based on the progress we got above.
const diff = nextState.at - lastState.at
const progressCurr = (progress - lastState.at) / diff
// Loop through all the colours. 'key' is the cutsom property name
Object.keys(lastState.colours).forEach(key => {
// We use hex codes for colours for convenience, but it's a lot easier to transition
// seperate Red, Green, Blue values so we convert them to a [R, G, B] array
const lastRGB = hexToRgb(lastState.colours[key])
const nextRGB = hexToRgb(nextState.colours[key])
// Get the new RGB by using 'lerping' to find the value between the last and next
// colours based on how far we are through the current animation.
// The lerp function doesn't necessarily return an int so we round it.
const currRGB = [\
Math.round(lerp(lastRGB[0], nextRGB[0], progressCurr)),\
Math.round(lerp(lastRGB[1], nextRGB[1], progressCurr)),\
Math.round(lerp(lastRGB[2], nextRGB[2], progressCurr))\
]
// Apply the custom property to root using the name and our new RGB value.
applyColour(key, currRGB)
})
}
// As we have two different animation 'modes', we change the function used to work
// out the progress depending on that mode. See the config above for how they work.
function getProgress() {
const d = new Date()
const progress = config.anims[animMode].getProgress(d)
return progress
}
// A slightly bewildering regular expression that turns a hex code into [R, G. B] array.
// Well-tested though so I don't need to touch it!
function hexToRgb(hex) {
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
return result ? [\
parseInt(result[1], 16),\
parseInt(result[2], 16),\
parseInt(result[3], 16)\
] : null
}
// Using 'linear interpolation' gets the value between the start and end values based on progress
function lerp(start, end, progress) {
return (1 - progress) * start + progress * end
}
// Uses name of custom property 'key' and [R, G, B] array and applies to root element
function applyColour(key, colour) {
const colourString = 'rgb(' + colour.join(',') + ')'
root.style.setProperty('--' + key, colourString)
}
// Round number to 'places' number of figures after decimal.
function round(num, places) {
const power = Math.pow(10, places)
return Math.round(num * power) / power
}
// Initialise and start animation.
function init() {
startAnim()
}
init()
Theme Picker
With the above code, we have an animated live colour scheme and the flexibility to extend it further. Let's do just that by creating methods to switch between 'dynamic' schemes and our named states.
We'll go through the basic code to change, and then a basic 'theme picker'.
Switching between dynamic states
In our configuration, we have set the progress function and interval for each dynamic theme. When we start the animation and when our updateAnim() function run, they use the value of animMode to choose the correct interval and progress function for the current mode.
This means all we need to do is stop the animation, change animMode, and start it again. For example to change to 'cycle':
endAnim()
animMode = 'cycle'
startAnim()
And likewise, to switch to 'live', we would do the same process but instead set animMode to 'live'.
Switching to a static 'named' state
We included the name property within our state so that we can refer to it when setting the theme. First we need to stop the animation, so that the dynamic state doesn't replace our changes when it next runs. Then, we need to find the colours for the state we would like to apply and apply them. We can do that with this short piece of code.
const theme = 'sunset'
endAnim()
const state = config.states.find(item => item.name === theme)
Object.keys(state.colours).forEach(key => {
applyColour(key, hexToRgb(state.colours[key]))
})
Line 3 uses the handy Array method 'find' which will return the item that matches our condition: where item.name equals our theme name.
We then loop through all the colours of that state and apply them as we did for our dynamic 'themes'.
Theme Picker
It's worth building out a theme picker for yourself, but here's a simple implementation to get us started:
<button data-active aria-pressed data-theme="live">Live</button>
<button data-theme="cycle">Cycle</button>
<button data-theme="sunrise">Sunrise</button>
<button data-theme="day">Day</button>
<button data-theme="sunset">Sunset</button>
<button data-theme="night">Night</button>
const themes = document.querySelectorAll('[data-theme]')
if (themes) {
themes.forEach(function(theme) {
theme.addEventListener('click', function(e) {
// remove active state from old theme buttons
themes.forEach(theme => {
theme.removeAttribute('data-active')
theme.removeAttribute('aria-pressed')
})
// add active state to clicked button
this.setAttribute('data-active', '')
this.setAttribute('aria-pressed', '')
// get slug for current theme
const themeSlug = this.getAttribute('data-theme')
// end animation
endAnim()
// if dynamic theme, set animMode, start animation and return
if (themeSlug === 'live' || themeSlug === 'cycle') {
animMode = themeSlug
startAnim()
return
}
// find theme state and apply the colours
const state = config.states.find(item => item.name === themeSlug)
Object.keys(state.colours).forEach(key => {
applyColour(key, hexToRgb(state.colours[key]))
})
})
})
}
Sun Animation
The final piece to our landscape is a moving sun. You would have thought it would be easy to implement, but it turned out to be more tricky than I first thought.
Lets go over our requirements:
- A small circle that is on the far left at 6, top at 12, right at 18, and bottom at 24.
- Respects screen width so it works for wide and narrow screens.
- Respects screen height so it is positioned in the right place compared to the landscape.
- Follows an ellipse based on the width and height
Due to all these reasons, my first thought of using animations becomes hard to implement. Respecting width, height and following an ellipse though sounds like a tricky challenge.
The solution ends up using our favourite feature the Custom Property, and exploiting the relationship between ellipses and the Sin function.
We can continue to keep our JavaScript minimal and respect the screen size by using transforms and elements the size of the screen. To our .landscape from the previous post:
<div class="landscape__sunWrap">
<div class="landscape__sun"></div>
</div>
$sun-size: min(4rem, 10vw);
$sun-movement-v: 30%;
$sun-movement-h: 40%;
.landscape {
&__sunWrap {
$distance: 10;
bottom: 10%;
height: 75%;
left: 0;
position: absolute;
transform: translateY(var(--scrollPos, 0));
width: 100%;
@media (prefers-reduced-motion: reduce) {
display: none;
}
}
&__sun {
height: 100%;
left: 0;
position: absolute;
top: 0;
transform:
translateX(calc(#{$sun-movement-h} * var(--sun-h)))
translateY(calc(#{$sun-movement-v} * var(--sun-v)));
width: 100%;
// the actual sun element
&::before {
background: #fff;
border-radius: 50%;
content: '';
height: $sun-size;
left: 50%;
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
width: $sun-size;
}
}
}
Using this code the positioning of our sun is based on rails, constrained by the size of our landscape. --sun-h and --sun-v are numbers between -1 and 1 which are used in the calc within our transform property to set how far up/down and left/right the sun is.
The advantage of using an element filling our landscape means that as the element is narrower, the less the sun moves horizontally. This leaves us with minimal JS:
function sunPos(progress) {
const sunWrap = document.querySelector('.landscape__sunWrap')
if (sunWrap) {
const sunH = -Math.sin(2 * Math.PI * progress / 24)
const sunV = -Math.sin(2 * Math.PI * (progress - 6) / 24)
sunWrap.style.setProperty('--sun-h', round(sunH, 3))
sunWrap.style.setProperty('--sun-v', round(sunV, 3))
}
}
This involves maths that I'm pretty sure I was taught in High School and University, but I am certain I have almost entirely forgotten! For a square element, this would create a circular movement but by splitting it up into separate components we have our ellipse.
We then run sunPos with our progress in our updateAnim() function and using the state.at property after setting a static theme.
Conclusion
If you've gotten this far, congratulations and thank you for sticking with me! Here's our final landscape, as above:
This is not the easiest post to read by any stretch of the imagination, but I wanted to get down a lot of info and I struggled to in a way that felt natural. Initial drafts were tutorial-like before I realised I was writing a 10,000 word tutorial!
I am planning to write more, but will be making them shorter and simpler than this one. Keep an eye out for future posts about:
- The 'hilly' dividers
- Blog setup with 11ty, esbuild and gulp
- Implementing mouse-based painting with canvas and JS
and a few more I have planned!
The post SVG Landscape with live colour theming - new site part 2 appeared first on alistairshepherd.uk.
]]>
Conditionally loading a polyfill for :focus-visible2021-05-21T00:00:00Zhttps://www.alistairshepherd.uk/writing/focus-visible-conditional-polyfill/The future of focus is :focus-visible! Although browser support is decent, Safari is still working on this important accessibility feature.
In the meantime, we can load the WICG focus-visible polyfill to offer improved focus styles in browsers that yet don't support it. Ideally we don't force browsers that support focus-visible to download a polyfill when it's unnecessary - and in future when all modern browsers support the feature, we don't want to ship that redundant code.
Here is a snippet we can use to only load the focus-visible polyfill if it isn't supported! Insert this before the closing </body> and change the script.src to point to your local copy of the polyfill (or use an asset CDN like jsdelivr).
<script>
try {
document.body.querySelector(':focus-visible');
} catch (error) {
var script = document.createElement('script');
script.src = "/js/focus-visible.js";
document.body.appendChild(script);
}
</script>
CSS
You'll also need to write CSS to handle focus indicators in three circumstances:
- focus-visible is supported;
- focus-visible not supported, but polyfill has been loaded;
- focus-visible not supported, polyfill not loaded.
This is my setup for these cases:
/**
* My focus styles
*/
:focus {
outline: 2px dashed currentColor;
outline-offset: .25rem;
}
/**
* When focus-visible is supported:
* remove outline when :focus but not :focus-visible
*/
:focus:not(:focus-visible) {
outline: none;
}
/**
* when polyfill loaded:
* remove outline when :focus but not .focus-visible
*/
.js-focus-visible :focus:not(.focus-visible) {
outline: none;
Notes
- This does add an additional script to load for browsers without support, but by adding the script as above loads it asyncronously. The browser will prioritise other resources, and the worst case is it isn't loaded by the first interaction and a fallback
:focusindicator is shown. - If you have a build step that minifies/optimises your CSS, you may find that the two
outline: nonedeclarations are combined into a single rule. Due to how the browser ignores any rules with selectors it doesn't understand, this won't work. You may need to disable optimisation on this step, or in my case I changed one of theoutline: nonedeclarations tooutline: 0. This CSS works the same, but means they won't be combined into a single rule by most minifiers.