Skip to content

Commit ef21f80

Browse files
Merge pull request #69 from chriswblake/cwb/fix-low-resolution-username-and-year
fix: low resolution username and year
2 parents e88e606 + 6a347bc commit ef21f80

File tree

2 files changed

+183
-172
lines changed

2 files changed

+183
-172
lines changed

stl/geometry/text.go

Lines changed: 159 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -10,109 +10,81 @@ import (
1010
"github.com/github/gh-skyline/types"
1111
)
1212

13-
// Common configuration for rendered elements
14-
type renderConfig struct {
15-
startX float64
16-
startY float64
17-
startZ float64
18-
voxelScale float64
19-
depth float64
20-
}
13+
const (
14+
baseWidthVoxelResolution = 2000 // Number of voxels across the skyline face
15+
voxelDepth = 1.0 // Distance to come out of face
2116

22-
// TextConfig holds parameters for text rendering
23-
type textRenderConfig struct {
24-
renderConfig
25-
text string
26-
contextWidth int
27-
contextHeight int
28-
fontSize float64
29-
}
17+
logoScale = 0.4 // Percent
18+
logoTopOffset = 0.15 // Percent
19+
logoLeftOffset = 0.03 // Percent
3020

31-
// ImageConfig holds parameters for image rendering
32-
type imageRenderConfig struct {
33-
renderConfig
34-
imagePath string
35-
height float64
36-
}
21+
usernameFontSize = 120.0
22+
usernameJustification = "left" // "left", "center", "right"
23+
usernameLeftOffset = 0.1 // Percent
3724

38-
const (
39-
imagePosition = 0.025
40-
usernameOffset = -0.01
41-
yearPosition = 0.77
42-
43-
defaultContextWidth = 800
44-
defaultContextHeight = 200
45-
textVoxelSize = 1.0
46-
textDepthOffset = 2.0
47-
frontEmbedDepth = 1.5
48-
49-
usernameContextWidth = 1000
50-
usernameContextHeight = 200
51-
usernameFontSize = 48.0
52-
usernameZOffset = 0.7
53-
54-
yearContextWidth = 800
55-
yearContextHeight = 200
56-
yearFontSize = 56.0
57-
yearZOffset = 0.4
58-
59-
defaultImageHeight = 9.0
60-
defaultImageScale = 0.8
61-
imageLeftMargin = 10.0
25+
yearFontSize = 100.0
26+
yearJustification = "right" // "left", "center", "right"
27+
yearLeftOffset = 0.97 // Percent
6228
)
6329

6430
// Create3DText generates 3D text geometry for the username and year.
65-
func Create3DText(username string, year string, innerWidth, baseHeight float64) ([]types.Triangle, error) {
31+
func Create3DText(username string, year string, baseWidth float64, baseHeight float64) ([]types.Triangle, error) {
6632
if username == "" {
6733
username = "anonymous"
6834
}
6935

70-
usernameConfig := textRenderConfig{
71-
renderConfig: renderConfig{
72-
startX: innerWidth * usernameOffset,
73-
startY: -textDepthOffset / 2,
74-
startZ: baseHeight * usernameZOffset,
75-
voxelScale: textVoxelSize,
76-
depth: frontEmbedDepth,
77-
},
78-
text: username,
79-
contextWidth: usernameContextWidth,
80-
contextHeight: usernameContextHeight,
81-
fontSize: usernameFontSize,
82-
}
83-
84-
yearConfig := textRenderConfig{
85-
renderConfig: renderConfig{
86-
startX: innerWidth * yearPosition,
87-
startY: -textDepthOffset / 2,
88-
startZ: baseHeight * yearZOffset,
89-
voxelScale: textVoxelSize * 0.75,
90-
depth: frontEmbedDepth,
91-
},
92-
text: year,
93-
contextWidth: yearContextWidth,
94-
contextHeight: yearContextHeight,
95-
fontSize: yearFontSize,
96-
}
97-
98-
usernameTriangles, err := renderText(usernameConfig)
36+
usernameTriangles, err := renderText(
37+
username,
38+
usernameJustification,
39+
usernameLeftOffset,
40+
usernameFontSize,
41+
baseWidth,
42+
baseHeight,
43+
)
9944
if err != nil {
10045
return nil, err
10146
}
10247

103-
yearTriangles, err := renderText(yearConfig)
48+
yearTriangles, err := renderText(
49+
year,
50+
yearJustification,
51+
yearLeftOffset,
52+
yearFontSize,
53+
baseWidth,
54+
baseHeight,
55+
)
10456
if err != nil {
10557
return nil, err
10658
}
10759

10860
return append(usernameTriangles, yearTriangles...), nil
10961
}
11062

111-
// renderText generates 3D geometry for the given text configuration.
112-
func renderText(config textRenderConfig) ([]types.Triangle, error) {
113-
dc := gg.NewContext(config.contextWidth, config.contextHeight)
63+
// renderText places text on the face of a skyline, offset from the left and vertically-aligned.
64+
// The function takes the text to be displayed, offset from left, and font size.
65+
// It returns an array of types.Triangle.
66+
//
67+
// Parameters:
68+
//
69+
// text (string): The text to be displayed on the skyline's front face.
70+
// leftOffsetPercent (float64): The percentage distance from the left to start displaying the text.
71+
// fontSize (float64): How large to make the text. Note: It scales with the baseWidthVoxelResolution.
72+
//
73+
// Returns:
74+
//
75+
// ([]types.Triangle, error): A slice of triangles representing text.
76+
func renderText(text string, justification string, leftOffsetPercent float64, fontSize float64, baseWidth float64, baseHeight float64) ([]types.Triangle, error) {
77+
// Create a rendering context for the face of the skyline
78+
faceWidthRes := baseWidthVoxelResolution
79+
faceHeightRes := int(float64(faceWidthRes) * baseHeight / baseWidth)
80+
81+
// Create image representing the skyline face
82+
dc := gg.NewContext(faceWidthRes, faceHeightRes)
83+
dc.SetRGB(0, 0, 0)
84+
dc.Clear()
85+
dc.SetRGB(1, 1, 1)
11486

115-
// Get temporary font file
87+
// Load font into context
11688
fontPath, cleanup, err := writeTempFont(PrimaryFont)
11789
if err != nil {
11890
// Try fallback font
@@ -121,31 +93,42 @@ func renderText(config textRenderConfig) ([]types.Triangle, error) {
12193
return nil, errors.New(errors.IOError, "failed to load any fonts", err)
12294
}
12395
}
124-
125-
if err := dc.LoadFontFace(fontPath, config.fontSize); err != nil {
96+
if err := dc.LoadFontFace(fontPath, fontSize); err != nil {
12697
return nil, errors.New(errors.IOError, "failed to load font", err)
12798
}
12899

129-
dc.SetRGB(0, 0, 0)
130-
dc.Clear()
131-
dc.SetRGB(1, 1, 1)
132-
dc.DrawStringAnchored(config.text, float64(config.contextWidth)/8, float64(config.contextHeight)/2, 0.0, 0.5)
133-
100+
// Draw text on image at desired location
134101
var triangles []types.Triangle
135102

136-
for y := 0; y < config.contextHeight; y++ {
137-
for x := 0; x < config.contextWidth; x++ {
103+
// Convert justification to a number
104+
var justificationPercent float64
105+
switch justification {
106+
case "center":
107+
justificationPercent = 0.5
108+
case "right":
109+
justificationPercent = 1.0
110+
default:
111+
justificationPercent = 0.0
112+
}
113+
114+
dc.DrawStringAnchored(
115+
text,
116+
float64(faceWidthRes)*leftOffsetPercent, // Offset from right
117+
float64(faceHeightRes)*0.5, // Offset from top
118+
justificationPercent, // Justification (0.0=left, 0.5=center, 1.0=right)
119+
0.5, // Vertically aligned
120+
)
121+
122+
// Convert context image pixels into voxels
123+
for x := 0; x < faceWidthRes; x++ {
124+
for y := 0; y < faceHeightRes; y++ {
138125
if isPixelActive(dc, x, y) {
139-
xPos := config.startX + float64(x)*config.voxelScale/8
140-
zPos := config.startZ - float64(y)*config.voxelScale/8
141-
142-
voxel, err := CreateCube(
143-
xPos,
144-
config.startY,
145-
zPos,
146-
config.voxelScale/2,
147-
config.depth,
148-
config.voxelScale/2,
126+
voxel, err := createVoxelOnFace(
127+
float64(x),
128+
float64(y),
129+
voxelDepth,
130+
baseWidth,
131+
baseHeight,
149132
)
150133
if err != nil {
151134
return nil, errors.New(errors.STLError, "failed to create cube", err)
@@ -161,34 +144,78 @@ func renderText(config textRenderConfig) ([]types.Triangle, error) {
161144
return triangles, nil
162145
}
163146

147+
// createVoxelOnFace creates a voxel on the face of a skyline by generating a cube at the specified coordinates.
148+
// The function takes in the x, y coordinates and height.
149+
// It returns a slice of types.Triangle representing the cube and an error if the cube creation fails.
150+
//
151+
// Parameters:
152+
//
153+
// x (float64): The x-coordinate on the skyline face (left to right).
154+
// y (float64): The y-coordinate on the skyline face (top to bottom).
155+
// height (float64): Distance coming out of the face.
156+
//
157+
// Returns:
158+
//
159+
// ([]types.Triangle, error): A slice of triangles representing the cube and an error if any.
160+
func createVoxelOnFace(x float64, y float64, height float64, baseWidth float64, baseHeight float64) ([]types.Triangle, error) {
161+
// Mapping resolution
162+
xResolution := float64(baseWidthVoxelResolution)
163+
yResolution := xResolution * baseHeight / baseWidth
164+
165+
// Pixel size
166+
voxelSize := 1.0
167+
168+
// Scale coordinate to face resolution
169+
x = (x / xResolution) * baseWidth
170+
y = (y / yResolution) * baseHeight
171+
voxelSizeX := (voxelSize / xResolution) * baseWidth
172+
voxelSizeY := (voxelSize / yResolution) * baseHeight
173+
174+
cube, err := CreateCube(
175+
// Location (from top left corner of skyline face)
176+
x, // x - Left to right
177+
-height, // y - Negative comes out of face. Positive goes into face.
178+
-voxelSizeY-y, // z - Bottom to top
179+
180+
// Size
181+
voxelSizeX, // x length - left to right from specified point
182+
height, // thickness - distance coming out of face
183+
voxelSizeY, // y length - bottom to top from specified point
184+
)
185+
186+
return cube, err
187+
}
188+
164189
// GenerateImageGeometry creates 3D geometry from the embedded logo image.
165-
func GenerateImageGeometry(innerWidth, baseHeight float64) ([]types.Triangle, error) {
190+
func GenerateImageGeometry(baseWidth float64, baseHeight float64) ([]types.Triangle, error) {
166191
// Get temporary image file
167192
imgPath, cleanup, err := getEmbeddedImage()
168193
if err != nil {
169194
return nil, err
170195
}
171196

172-
config := imageRenderConfig{
173-
renderConfig: renderConfig{
174-
startX: innerWidth * imagePosition,
175-
startY: -frontEmbedDepth / 2.0,
176-
startZ: -0.85 * baseHeight,
177-
voxelScale: defaultImageScale,
178-
depth: frontEmbedDepth,
179-
},
180-
imagePath: imgPath,
181-
height: defaultImageHeight,
182-
}
183-
184197
defer cleanup()
185198

186-
return renderImage(config)
199+
return renderImage(
200+
imgPath,
201+
logoScale,
202+
voxelDepth,
203+
logoLeftOffset,
204+
logoTopOffset,
205+
baseWidth,
206+
baseHeight,
207+
)
187208
}
188209

189210
// renderImage generates 3D geometry for the given image configuration.
190-
func renderImage(config imageRenderConfig) ([]types.Triangle, error) {
191-
reader, err := os.Open(config.imagePath)
211+
func renderImage(filePath string, scale float64, height float64, leftOffsetPercent float64, topOffsetPercent float64, baseWidth float64, baseHeight float64) ([]types.Triangle, error) {
212+
213+
// Get voxel resolution of base face
214+
faceWidthRes := baseWidthVoxelResolution
215+
faceHeightRes := int(float64(faceWidthRes) * baseHeight / baseWidth)
216+
217+
// Load image from file
218+
reader, err := os.Open(filePath)
192219
if err != nil {
193220
return nil, errors.New(errors.IOError, "failed to open image", err)
194221
}
@@ -199,34 +226,32 @@ func renderImage(config imageRenderConfig) ([]types.Triangle, error) {
199226
fmt.Println(closeErr)
200227
}
201228
}()
202-
203229
img, err := png.Decode(reader)
204230
if err != nil {
205231
return nil, errors.New(errors.IOError, "failed to decode PNG", err)
206232
}
207233

234+
// Get image size
208235
bounds := img.Bounds()
209-
width := bounds.Max.X
210-
height := bounds.Max.Y
211-
212-
scale := config.height / float64(height)
236+
logoWidth := bounds.Max.X
237+
logoHeight := bounds.Max.Y
213238

239+
// Transfer image pixels onto face of skyline as voxels
214240
var triangles []types.Triangle
215-
216-
for y := height - 1; y >= 0; y-- {
217-
for x := 0; x < width; x++ {
241+
for x := 0; x < logoWidth; x++ {
242+
for y := logoHeight - 1; y >= 0; y-- {
243+
// Get pixel color and alpha
218244
r, _, _, a := img.At(x, y).RGBA()
245+
246+
// If pixel is active (white) and not fully transparent, create a voxel
219247
if a > 32768 && r > 32768 {
220-
xPos := config.startX + float64(x)*config.voxelScale*scale
221-
zPos := config.startZ + float64(height-1-y)*config.voxelScale*scale
222-
223-
voxel, err := CreateCube(
224-
xPos,
225-
config.startY,
226-
zPos,
227-
config.voxelScale*scale,
228-
config.depth,
229-
config.voxelScale*scale,
248+
249+
voxel, err := createVoxelOnFace(
250+
(leftOffsetPercent*float64(faceWidthRes))+float64(x)*scale,
251+
(topOffsetPercent*float64(faceHeightRes))+float64(y)*scale,
252+
height,
253+
baseWidth,
254+
baseHeight,
230255
)
231256

232257
if err != nil {

0 commit comments

Comments
 (0)