-
Notifications
You must be signed in to change notification settings - Fork 13
New Seasonal Valentine's Theme #126
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 5 commits
361df69
43df4c6
91c79f3
b8d2dbd
726da74
c663390
ebd072f
3aefa9a
0a48f6f
5ab2b7f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,203 @@ | ||
| const BasicColorTheme = require('./templates/BasicColorTheme') | ||
| const ColorSchemeTransformations = require('./ColorSchemeTransformations') | ||
| const BasicTiming = require('./timings/BasicTiming') | ||
|
|
||
| const hearts = new Set() | ||
|
|
||
| function drawHeart (ctx, x, y, size, rotation) { | ||
| ctx.save() | ||
| ctx.translate(x, y) | ||
| ctx.rotate(rotation) | ||
| ctx.beginPath() | ||
| const topCurveHeight = size * 0.3 | ||
| ctx.moveTo(0, topCurveHeight) | ||
| // Left curve | ||
| ctx.bezierCurveTo( | ||
| 0, 0, | ||
| -size / 2, 0, | ||
| -size / 2, topCurveHeight | ||
| ) | ||
| ctx.bezierCurveTo( | ||
| -size / 2, (topCurveHeight + size) / 2, | ||
| 0, (topCurveHeight + size) / 2, | ||
| 0, size | ||
| ) | ||
| // Right curve | ||
| ctx.bezierCurveTo( | ||
| 0, (topCurveHeight + size) / 2, | ||
| size / 2, (topCurveHeight + size) / 2, | ||
| size / 2, topCurveHeight | ||
| ) | ||
| ctx.bezierCurveTo( | ||
| size / 2, 0, | ||
| 0, 0, | ||
| 0, topCurveHeight | ||
| ) | ||
| ctx.closePath() | ||
| ctx.restore() | ||
| } | ||
|
|
||
| // Helper function to draw a heart path (reusable for both background and foreground) | ||
| function drawHeartPath (ctx, w, h, yOffset) { | ||
| ctx.beginPath() | ||
| ctx.moveTo(0, h * 0.45 + yOffset) | ||
| ctx.bezierCurveTo( | ||
| -w * 0.4, h * 0.15 + yOffset, | ||
| -w, h * 0.05 + yOffset, | ||
| -w, -h * 0.15 + yOffset | ||
| ) | ||
| ctx.bezierCurveTo( | ||
| -w, -h * 0.42 + yOffset, | ||
| -w * 0.5, -h * 0.5 + yOffset, | ||
| 0, -h * 0.3 + yOffset | ||
| ) | ||
| ctx.bezierCurveTo( | ||
| w * 0.5, -h * 0.5 + yOffset, | ||
| w, -h * 0.42 + yOffset, | ||
| w, -h * 0.15 + yOffset | ||
| ) | ||
| ctx.bezierCurveTo( | ||
| w, h * 0.05 + yOffset, | ||
| w * 0.4, h * 0.15 + yOffset, | ||
| 0, h * 0.45 + yOffset | ||
| ) | ||
| ctx.closePath() | ||
| } | ||
|
|
||
| function drawHeartCountdown (ctx, x, y, radius, proportion) { | ||
| const heartWidth = radius * 2 | ||
| const heartHeight = radius * 2 | ||
| const w = heartWidth * 0.5 | ||
| const h = heartHeight * 0.95 | ||
| const yOffset = h * 0.02 | ||
| // ===== DRAW BACKGROUND HEART (so they can see that its supposed to be a heart) ===== | ||
| ctx.save() | ||
| ctx.translate(x, y) | ||
| ctx.globalAlpha = 0.5 | ||
| drawHeartPath(ctx, w, h, yOffset) | ||
| ctx.fill() | ||
| ctx.restore() | ||
| // ===== DRAW FOREGROUND HEART (countdown, clipped) ===== | ||
| ctx.save() | ||
| ctx.beginPath() | ||
| ctx.arc(x, y, radius * 1.2, (Math.PI / -2), (Math.PI / -2) + (-2 * Math.PI) * (1 - proportion), true) | ||
| ctx.lineTo(x, y) | ||
| ctx.closePath() | ||
| ctx.clip() | ||
| ctx.globalAlpha = 0.98 | ||
| ctx.fillStyle = 'rgba(255,255,255,0.98)' | ||
| ctx.translate(x, y) | ||
| drawHeartPath(ctx, w, h, yOffset) | ||
| ctx.fill() | ||
| ctx.restore() | ||
| } | ||
|
|
||
| module.exports = { | ||
| name: 'Valentines', | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: "Valentine's"
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will fix! |
||
| drawHeartCountdown: drawHeartCountdown, | ||
| enabled: (secrets) => { | ||
| const now = new Date() | ||
| return (now.getMonth() + 1) === 2 // February | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd expect Valentine's day theme to be only on Valentine's day
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Or maybe if Valentine's day is on the weekend we could do the week leading up to it. Continuing to show Valentine's day after it has passed feels odd.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, but I also want users to see the theme for more then one day? How about Feb 9-Feb 19? That gives them a 10 day window with Valentine's being directly in the middle of that.
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How about 10 days ending on Valentine's day? That way it won't continue occurring after Valentines' day which I think is odd. That would also match the behavior of the Halloween theme well: https://github.com/nicolaschan/bell/blob/6727149a96c7d0095b74c06cdc2deaa048dedc63/src/themes/Halloween.js#L9C1-L9C63 |
||
| }, | ||
| specialEffects: (ctx, canvas) => { | ||
| if (hearts.size < 30 && Math.random() < 0.08) { | ||
| hearts.add({ | ||
| x: Math.random() * window.innerWidth, | ||
| y: -20, | ||
| size: 30 + Math.random() * 30, | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Idk, I thought this looked nice but I can see how it might be intrusive. I'll model the hearts to be underneath the text. |
||
| rotation: Math.random() * Math.PI * 2, | ||
| rotationSpeed: (Math.random() - 0.5) * 0.05, | ||
| xSpeed: (Math.random() - 0.5) * 1.5, | ||
| ySpeed: 1 + Math.random() * 2, | ||
| color: Math.random() > 0.5 ? '#ff006e' : '#ff4d6d', | ||
| opacity: 0.6 + Math.random() * 0.4, | ||
| clicked: false, | ||
| pulsePhase: Math.random() * Math.PI * 2 | ||
| }) | ||
| } | ||
|
|
||
| for (const heart of hearts) { | ||
| ctx.globalAlpha = heart.opacity | ||
| ctx.fillStyle = heart.clicked ? '#ffffff' : heart.color | ||
|
|
||
| // Calculate pulse scale (subtle pulse between 0.9 and 1.1) | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No need to comment obvious code. Such comments don't add much value and tend to get out of date with changes, which I think this one already is because the next line looks like it will vary between
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's because I changed the pulse scale later and forget to fix the comment. Regardless, I'll remove the comment. |
||
| const pulseScale = 1 + Math.sin(heart.pulsePhase) * 0.3 | ||
| const currentSize = heart.clicked ? heart.size * 1.5 : heart.size | ||
| drawHeart(ctx, heart.x, heart.y, currentSize * pulseScale, heart.rotation) | ||
| ctx.fill() | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this be part of drawing the heart in |
||
| if (heart.y > window.innerHeight + 50) { | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the magic |
||
| hearts.delete(heart) | ||
| } | ||
| heart.y += heart.ySpeed | ||
| heart.x += heart.xSpeed | ||
| heart.rotation += heart.rotationSpeed | ||
| heart.pulsePhase += 0.05 | ||
| } | ||
| ctx.globalAlpha = 1.0 | ||
| canvas.addEventListener('mousedown', function (e) { | ||
| for (const heart of hearts) { | ||
| const distance = Math.sqrt(Math.pow(heart.x - e.x, 2) + Math.pow(heart.y - e.y, 2)) | ||
| if (distance < heart.size) { | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Trying this out I noticed that the hitbox doesn't match the heart size. When I clicked large hearts near the edge but still within the heart it didn't register. Looking at this code I think that's because |
||
| heart.clicked = !heart.clicked | ||
| break | ||
| } | ||
| } | ||
| }) | ||
| }, | ||
| theme: BasicColorTheme( | ||
| ColorSchemeTransformations.fromObjectStrings, | ||
| BasicTiming, | ||
| [{ | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Some of these color choices have very low contrast which makes it hard to read, especially if you have poor eyesight, bad lighting, or a bad screen. Can we check each color combination and ensure all of the text on all of the combinations are easily readable? You can use this in the JS console to mock the time to check them: bellTimer.correctedDate.enableDevMode(new Date('2026-02-16 9:21'), 0)
|
||
| background: { | ||
| 'background-image': 'linear-gradient(135deg, #ff6b9d 0%, #ffc3e0 100%)', | ||
| 'background-color': '#ff85b3' | ||
| }, | ||
| text: '#8b0028', | ||
| subtext: '#c41e5c', | ||
| contrast: 'white' | ||
| }, { | ||
| background: { | ||
| 'background-image': 'linear-gradient(135deg, #ff1f5a 0%, #ff6b9d 100%)', | ||
| 'background-color': '#ff4578' | ||
| }, | ||
| text: '#8b0028', | ||
| subtext: '#c41e5c', | ||
| contrast: 'white' | ||
| }, { | ||
| background: { | ||
| 'background-image': 'linear-gradient(135deg, #c9184a 0%, #ff4d6d 100%)', | ||
| 'background-color': '#e6335b' | ||
| }, | ||
| text: '#ffffff', | ||
| subtext: '#ffc3e0', | ||
| contrast: 'white' | ||
| }, { | ||
| background: { | ||
| 'background-image': 'linear-gradient(135deg, #ff4d6d 0%, #ff758f 100%)', | ||
| 'background-color': '#ff617e' | ||
| }, | ||
| text: '#ffffff', | ||
| subtext: '#ffc3e0', | ||
| contrast: 'white' | ||
| }], { | ||
| holiday: { | ||
| background: { | ||
| 'background-image': 'linear-gradient(135deg, #ff006e 0%, #ff85b3 100%)', | ||
| 'background-color': '#ff4390' | ||
| }, | ||
| text: '#ffffff', | ||
| subtext: '#ffc3e0', | ||
| contrast: 'white' | ||
| }, | ||
| weekend: { | ||
| background: { | ||
| 'background-image': 'linear-gradient(135deg, #ff006e 0%, #ff85b3 100%)', | ||
| 'background-color': '#ff4390' | ||
| }, | ||
| text: '#ffffff', | ||
| subtext: '#ffc3e0', | ||
| contrast: 'white' | ||
| } | ||
| } | ||
| ) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| const m = require('mithril') | ||
| const Valentines = require('../themes/Valentines') // To replace the circle with a heart on Valentines theme | ||
|
|
||
| var getIconImage = function (min) { | ||
| var faviconColors = { | ||
|
|
@@ -79,11 +80,18 @@ var updateGraphics = function (vnode) { | |
| var posX = width / 2 | ||
| var posY = height / 2 | ||
|
|
||
| ctx.beginPath() | ||
| ctx.arc(posX, posY, radius, (Math.PI / -2), (Math.PI / -2) + (-2 * Math.PI) * (1 - proportion), true) | ||
| ctx.lineTo(posX, posY) | ||
| ctx.closePath() | ||
| ctx.fill() | ||
| // Check if current theme is Valentines | ||
| if (themeManager.currentTheme.name === 'Valentines') { | ||
| // Draw heart countdown instead of circle - same size and behavior | ||
| Valentines.drawHeartCountdown(ctx, posX, posY, radius, proportion) | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I appreciate the creativity but there are a few problems with this that make me think we should keep the circle.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yea, I see how that might be hard to read when the theme gets darker. I'll change it back to a circle, it was more of a last-minute idea and not really that important to me. |
||
| } else { | ||
| // Draw the normal circle countdown | ||
| ctx.beginPath() | ||
| ctx.arc(posX, posY, radius, (Math.PI / -2), (Math.PI / -2) + (-2 * Math.PI) * (1 - proportion), true) | ||
| ctx.lineTo(posX, posY) | ||
| ctx.closePath() | ||
| ctx.fill() | ||
| } | ||
| } | ||
| const updateSnowflakes = function (vnode) { | ||
| const c = vnode.dom | ||
|
|
||



There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is useful but can we separate it into another PR? I think we should actually solve this with the existing
enableDevModeso that you can mock the time and see the themes available. This would let you E2E test that your theme appears on the right dates too.Right now this is how I mock the time in the JS console:
We should update the theme available function in the seasonal themes to take the actual bellTimer instance instead of using
new Date().There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ohh, didn't see this in the codebase. I'll remove the debug mode.